The Evolution of C#: What’s Changed and What’s Ahead

By: Ricardo
  |  May 3, 2018
The Evolution of C#: What’s Changed and What’s Ahead

Recently Microsoft made several changes to the C# language. Some of those may even seem quite strange – definitely not your daddy’s C# anymore

The truth is, even though most of this is just “syntactic sugar” – has no equivalent in IL, in the same way that LINQ didn’t require any changes in IL too, the language is changing very rapidly.  Of course, we don’t have to use all of the new features at once, but with time, we may come to the conclusion that some of them are really quite useful.

This article describes what changed in C# since version 7.0, which was released in the Fall of 2016, and also shows a glimpse of the future versions 7.3 and 8.0. Before 7 we had version 6, which was also feature-rich, but that was a long time ago.

Do not forget that in order to get access to the latest features, you will need to enable support for them in Visual Studio:

As you can see, you can pick any version you want, or you can use whatever the latest one is. The special versions default and latest resolve to the latest major and minor language versions installed on the machine, respectively.

Please keep in mind that features of unreleased versions – 7.3 and 8.0 – are possible to change before they are actually released, due to time or other constraints.

C# 7.0

Version 7 has been around for quite some time, and I believe some of its features have already gotten into our development habits. Let’s see them one by one.

Out variables

In the past, variables passed to an out parameter had to be declared first; now this is no longer the case, which results in slightly simpler code. Where we had to do this:

DateTime date; 
 
DateTime.TryParse(dateString, out date);

Now we can have instead:

DateTime.TryParse(dateString, out var date);

Notice two things: the date variable is now declared in the scope of the current method or property, and we can use the var keyword to automatically infer its type.

Tuples

This one is probably everyone’s favorite: the ability to use tuples in C#. Tuples exist in other languages such as F# or Python, and are essentially a lightweight way to build types with just a couple of fields.

Tuples can have anonymous fields:

var temperature = (5, 41);

Or they can have names:

var temperature = (celsius: 5, fahrenheit: 41);

A variable can be declared without actually assigning it a value:

(double, double) temperature; // unnamed tuple 
 
(double celsius, double fahrenheit) temperature; // named tuple 
 
temperature.celsius = 5; 
 
temperature.Item2 = 41; //even for named tuples we can still use the ItemX notation

They can be used as the return type of methods:

public (double, double) GetTemperature()

Assigning a method return to a variable is called deconstruction:

(double celsius, double fahrenheit) = GetTemperature();

Depending on whether they are anonymous or not, its fields can be accessed directly, in the same way as a class or struct:

var celsius = temperature.celsius; // named tuple

var fahrenheit: temperature.Item2; // unnamed tuple

Notice that in the case of tuples with unnamed fields, they get automatically called Item1, Item2 and so on.

Of course, you can declare extension methods too:

public static (double, double) IncrementCelsius(this (double celsius, double fahrenheit) temperature, double celsiusDegrees) 
{ 
    return (temperature.celsius + celsiusDegrees, ((temperature.celsius + celsiusDegrees) * 9.5 + 32)); 
}

Finally, you can also assign named tuples to unnamed ones, provided you respect the field types:

(double, double) temperature; 
 
(double celsius, double fahrenheit) temp2 = temperature;

Another aspect is, you can “deconstruct” any type into a tuple. For that, you need to provide a Deconstruct method with the appropriate parameters. For example, consider this class:

public class Location 
{ 
    public int X { get; set; } 
    public int Y { get; set; } 
    public void Deconstruct(out int x, out int y) { x = this.X; y = this.Y; } 
}

You can then do:

var loc = new Location { X = 10, Y = 20 }; 
var (x, y) = loc;

And the tuple will be populated automatically! You can have any number of Deconstruct methods you want, as long as their signature is different, this will allow deconstructing to different tuple structures.

So, what are tuples useful for; can’t we just use classes or structs instead? We sure can, but this way it’s easier as we don’t have to declare the type. Just don’t forget that we need to add the System.ValueTuple NuGet package.

Discards

Discard variables are just that: variables that you don’t care about. You can pass them inside tuples, or for out parameters:

var (celsius, _) = GetTemperature();

if (DateTime.TryParse(dateString, out var _) { }

Keep in mind that you can’t declare multiple discard (_) variables on the same scope, but you can do so in the same tuple deconstruction.

ref locals and returns

The ref keyword can be used to return values from a method. We have other alternatives, such as out parameters or tuples, but ref has been around since the beginning of .NET and is especially useful if the return value is conditional, for example, if a conversion cannot be done. Now we also have return ref types.

Return ref values are particularly useful if we wish to return a pointer to an array position:

private int[] _array; 
 
public ref int ArrayValue(int index) 
{ 
    return ref _array[index]; 
}

ref var val = ref ArrayValue(1); 
val++;

Notice the multiple uses of the ref keyword: not just in the method, but also on the variable declaration and assignment. Also, the declaration of a ref variable must be followed by an assignment of a ref method call, in pretty much the same way as a var declaration.

Local functions

It is now possible to declare functions inside methods, property bodies, or even constructors. Why is that, I hear you ask; aren’t lambdas enough? Well, lambdas cannot have attributes or ref/out parameters. Local functions are just like any other type-level method, but only with a more narrow scope.

public static void Main() 
{ 
    int Add(int a, int b) => return a + b; 
 
    bool Divide(int a, int b, out double result) 
    { 
        if (b != 0) { result = a / b; return true; } 
        result = 0; return false; 
    } 
 
    var result1 = Add(1, 2); 
    Divide(1, 2, out var result); 
}

More expression-bodied members

Remember expression-body properties? They allow you to return simple (or not so simple) expressions without the drag of { }:

private DateTime _date; 
public DateTime Date => _date;

Now you can do the same with get or set property, methods, constructors, and finalizers:

public DateTime Birthday 
{ 
    get => _birthday; 
    set => _birthday = value; 
} 
public int Age() => (int) (DateTime.Today - _birthday).TotalDays / 365; 
public Person(string name) => this.Name = name; 
~Person() => Console.WriteLine("Finalizer called");

This is just a simplified syntax, nothing really new here, but still nice to have.

Pattern matching

The syntax for the switch keyword and for comparisons has been greatly enhanced; whereas before all we could do was exact matching, now we have additional patterns that we can check for:

object o = 100; 
if (o is int val) { } 
else if (o is null) { }

As you can see, we can declare a variable that will hold the cast value, in case it is of the specified type.

For switches, it gets even better, as we can add a condition to it:

switch (o) 
{ 
case int even when (even % 2) == 0: 
    break; 
case int odd: 
    break; 
}

Throw expressions

We can now use throw expressions in lambdas or expression bodies:

Func<int, int, double> result = (dividend, divisor) => divisor == 0 ? throw new ArgumentException("Divisor cannot be null") : dividend / divisor; 
 
public int Property 
{ 
    get: return _property; 
    set: _property = (value < 0) ? throw new ArgumentException("Value cannot be negative") : value; 
}

Generalized async return types

Sometimes an async method returns a value type, which does not need to be allocated on the heap. But because Task and Task<T> classes are reference types, they are always allocated there. Realizing this, Microsoft introduced the ValueTask<T> struct, which provides a better alternative. The syntax is exactly the same:

public async ValueTask GetTemperatureCelsius() 
{ 
    return 5; 
}

There’s a constructor in ValueTask<T> that takes a Task<T> parameter, this is how you can return values coming from “legacy” asynchronous methods:

public async Task GetTemperatureAsync() { } 
 
return new ValueTask(GetTemperatureAsync());

You’ll need to add a reference to the System.Threading.Tasks.Extensions NuGet package.

Numeric literal syntax improvements

This one is really only about readability: we can use the _ character to separate digits in any of the numeric formats we have:

var pi = 3.141_592_653_589; 
var sixtyFour = 0b0100_0000; 
var billion = 1_000_000_000;

Remember that this is just for visualization, it doesn’t impact at all the actual value being used.

C# 7.1

This one does not bring as much improvements as 7.0, but here they are.

Async Main

The Main method can now be made asynchronous by adding the async keyword. If you combine it with the expression body members feature, you can have it like this:

static async Task Main() => await Something();

Interestingly, as of now, you can return both Task or Task<int>, but not ValueTask<int>. Anyway, with this approach, you can go asynchronous all the way!

Default literal expressions

Sometimes, returning the default value for a generic type can be tedious. For example, consider this:

Func<int, int, int> operation = default(Func<int, int, int>);

Now you can just write:

Func<int, int, int> operation = default;

This also works with parameters too, of course, and with simple types:

public int Increment(int value, int amount = default) => value + amount;
int i = default;

Inferred tuple element names

This one is a small improvement, and works in pretty much the same way as anonymous types: it can infer tuple members from the variable names being passed. For example, say you have this:

string name = "Ricardo"; 
int yearOfBirth = 1975;

You create a tuple like this:

var data = (name, yearOfBirth);

And the tuple member names will be called, respectively, name and yearOfBirth. Note that, of course, this only works when you use variables or properties to initialize the tuple, not methods or complex expressions.

C# 7.2

Another small number of changes, mostly related to performance.

Reference semantics with value types

A couple of changes were introduced that aim to provide better performance with structs (which are copied byte by byte, remember). The changes are:

  • in modifier for value type parameters: the same as combining readonly and ref. It should provide better performance as the value type won’t be copied byte by byte
double Distance(in Point p1, in Point p2)
  • ref readonly: a return value for a method can be marked as ref readonly meaning that it is returned by reference but not modifiable, e.g., one cannot assign another value to the variable where it is kept. Of course, any members of the struct can be modified, and since they are passed by reference, it is the source that is changed. Conceptually the same as having the ref struct and readonly struct modifiers
Point _currentPos; 
    ref readonly Point GetCurrentPosition() => ref _currentPos; 
    ref readonly Point pos = ref GetCurrentPosition();
  • readonly struct: these structs cannot be modified and therefore cannot have publicly settable members
readonly struct ImmutableLocation 
{ 
    public ImmutableLocation(int x, int y) { X = x; Y = y; } 
    public int X { get; } 
    public int Y { get; } 
}
  • ref struct: a struct that is stored in the stack and can never be created as a member of a reference type and kept in the heap. This was conceived primarily because of Span<T> (will be introduced in C# 8), and what it means is that you can never box it in a object or dynamic reference. This is somewhat hard to understand unless you want to dig into high-performance code and unmanaged references. This is not allowed:
public readonly ref struct Span { ... } 
Span bytes = ...; 
object pointer = byes; // does not compile

public struct ManagedStruct 
{ 
    public Span Bytes { get; set; } // does not compile 
} 
public ref struct ManagedRefStruct 
{ 
    public Span Bytes { get; set; } //ok 
}

Some of these modifiers have bigger implications, so I advise you have a look at the official documentation here.

Non-trailing named arguments

Before this version, we could only declare named arguments after all mandatory ones had values. Now it is no longer the case, and we can have arguments in any order:

public static int Sum(int a, int b) => a + b; 
Sum(1, b: 2);

Read all about named and optional arguments here.

Leading underscores in numeric literals

Version 7.1 introduced underscores in numeric literals, but left out the capability to add them as the first character. This functionality now brings this to C#.

var byteValue = 0b_1111_0000;

Private protected access modifier

Private protected members existed since the specification of the .NET CLR, but no .NET language has had support for it, until now. Essentially, this accessibility level lets derived types access the member, but only if they belong in the same containing assembly. See the other access modifiers here.

Ref extension methods on structs

Seems weird, but this is now possible:

public struct MyStruct { } 
 
public static class StructExtensions 
{ 
    public static void Update(ref this MyStruct r) 
    { 
        r = new MyStruct(); // replaces the original variable's value 
    } 
}

This is thus valid code:

var x = new MyStruct(); 
x.Update(); // x now becomes something else!

This works for any struct, not just ref structs, but, alas, won’t work with reference types.

C# 7.3

This version will be available with Visual Studio 17 15.7, which is currently in preview, so we can already test it. Again, some features are related to performance, as it’s now a very hot topic at Microsoft.

Auto-implemented property field-targeted attributes

You may be aware that auto properties which have been around for quite a while have indeed a backing field to support them. But before this feature, we could not apply attributes to these backing fields, only to the properties themselves. Now this will be possible:

[field: NonSerialized] 
public string Password { get; set; }

Type constraints unmanaged, enum and delegate

This feature introduces a new constraint for generic parameters: unmanaged. This will be usable where new(), class and struct are, but we won’t be able to combine it with any of them.

void Process(T [] data) where T : unmanaged { }

This will only be useful for the few of us working with unmanaged memory. T itself has to be a struct and its fields can only consist of:

  • Types sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, IntPtr or UIntPtr
  • Enumerations
  • Unmanaged pointer types (T *)
  • Other user-defined struct that satisfy the unmanaged constraint

Two additional new constraints are enum and delegate, which do exactly what they seem: allow only enumerated or delegate types to be passed as generic parameters.

void Call(T action) where T : delegate { } 
T Parse(string enumeratedValue) where T : enum { }

Expression variables in initializers

This is a complement to the initializer syntax for constructors, methods, fields and properties where out/ref could not be used. Now, this is valid syntax:

public bool IsValidDate(string str) => DateTime.TryParse(str, out var date);

ref reassignment

It shall be possible to reassign values to ref variables, even inside loops:

ref VeryLargeStruct reflocal = ref veryLargeStruct;

Improved overload candidates

Three new rules for compiler overload disambiguation:

  1. When a method group contains both instance and static members, we discard the instance members if invoked without an instance receiver or context, and discard the static members if invoked with an instance receiver. When there is no receiver, we include only static members in a static context, otherwise both static and instance members. When the receiver is ambiguously an instance or type due to a color-color situation, we include both. A static context, where an implicit this instance receiver cannot be used, includes the body of members where no this is defined, such as static members, as well as places where this cannot be used, such as field initializers and constructor-initializers.
  2. When a method group contains some generic methods whose type arguments do not satisfy their constraints, these members are removed from the candidate set.
  3. For a method group conversion, candidate methods whose return type doesn’t match up with the delegate’s return type are removed from the set.
  4. When calling a method that has an overload with an in parameter and one without it, if the expression is an lvalue, we bind to the overload with the in parameter, otherwise, to the other.

Indexing fixed fields should not require pinning

This one is tricky and can be regarded as a relaxation of the specification. Essentially, it won’t be necessary to pin (fix) explicitly variables that point to fixed fields:

unsafe struct S 
{ 
    public fixed int myFixedField[10]; 
} 
 
class Program 
{ 
    static S s; 
 
    unsafe static void Main() 
    { 
        var p = s.myFixedField[5]; // no fixed required 
    } 
}

Don’t worry too much about this one, unless you are working with unmanaged code and fixed memory locations.

Pattern-based fixed statement

A fixed statement, which prevents the garbage collector from claiming a heap allocation, can now be used as this:

fixed (byte* ptr = GetUnmanagedPointer()) 
{ 
    // something 
}

Again, a feature only helpful for developers working with unmanaged code.

Stackalloc array initializers

This will now be possible:

stackalloc int[3] { 1, 2, 3 };

Tuple equality

Because tuples (introduced in C# 7.0) are automatically generated, they do not implement the == and != operators, which means the compiler relies on reference equality (same object). Or, they don’t, until this feature is implemented, when it does, this will work:

var equal = (10, "abc") == (20, "xyz"); // false

What will happen is, every field will be compared for equality/inequality, and only if they are all considered equal will the test for the tuple succeed.

C# 8.0

This is the next future major version of .NET. It is still unclear when it will be released – not this year, for sure – but it’s an ambitious one and one likely to cause some controversy, especially because of nullable reference types. Lots of new features, but let’s go one by one.

Relax ordering constraints around ‘ref’ and ‘partial’ modifiers on type declarations

This feature will make it possible to use partial and ref keywords in any position, in a type declaration:

public class partial MyClass { }

public ref struct MyStruct { }

Permit stackalloc in nested expressions

This one may not be very easy to understand and only a few will be able to use it. The stackalloc keyword allows the allocation of a block of memory on the stack, when used in unsafe C# code. This currently can only be used in local variable initializers, but this feature will allow its usage in other places as well.

Permit ‘t is null’ for unconstrained type parameter

Another generic parameter constraint, this time, for enforcing the parameter to be nullable. It will go somewhat like this:

public class GenericContainer where T is null 
{ 
    public virtual T GetItem() => null; 
}

It’s slightly better than returning default, among other reasons, because the default value of a type other than a reference one is not always easy to compare and null is a very specific value.

Allow ‘default’ in deconstruction

Another feature related to tuples, this time, allowing the default keyword when declaring a tuple:

(int x, int y) = default;

Slicing / range

If this one comes to life, we will have a syntax similar to Pascal’s for defining ranges:

var array = new int[SIZE]; 
var slice = array[500:1000];

Notice that the actual syntax may change!

Native-sized number types

When this feature is implemented, it shall be possible to declare integer types of the architecture’s “natural” size (e.g., 32 bytes for x86, 64 for x64). A new type (or types, because of unsigned) nint will be introduced. This will result in better performance in some cases and we won’t have to know about the architecture where the code is running.

Fixed-size buffers

This feature will permit indexing fixed-size buffers in managed code. The declaration will be like this:

public fixed UNMANAGED_STRUCTURE Buffer[1025];

Negated-condition if statement

A controversial proposal to add negated semantics to if statements:

if !(condition) { }

or an alternative syntax:

if not (condition) { }

Again, this is not something that we can’t achieve right now.

Pattern matching

Pattern-matching (introduced in C# 7.0 via the is keyword) will be augmented, for types:

if (expression is MyType) { }

for constant values (literals, types, enumerated values, nulls):

if (expression is null) { }

for variable extraction:

if (expression is var int) { }

Null-coalescing assignments

This one will make it possible to replace code like this:

if (variable == null) 
{ 
    variable = expression; 
}

By this:

variable ??= expression;

Null-conditional await

C# 6 introduced the null-conditional operator:

myVar?.MyMethod();

If you remember, MyMethod will only be called if myVar is not null. This proposal brings the same to awaitable variables, the syntax might look like this:

await? task;

Target-typed ‘new’ expression

Another code saver: this will prevent unambiguous instantiations to skip the type name, like this:

Point p = new (10, 10);

Mix declarations and variables in deconstruction

When working with tuples, the following will be permitted:

var x; 
(var w, x) = (0, 0);

Notice how we are mixing a variable (x) with a new declaration (w).

Permit ternary operation with int? and double operands

This feature will permit converting from nullable int into nullable double without the need for any cast, making this code compile:

int? nullableInt = 10; 
double floating = 5; 
var result = condition ? nullableInt : floating;

Not huge, just making the compiler somewhat smarter and sparing us some code.

Nullable-enhanced common type

Similar to the previous future, here’s another code saver. When we have a ternary expression in the form of:

var myVariable = condition ? null : 100;

we currently must add an explicit cast to null so that the compiler can infer the type of the implicit variable we’re declaring:

var myVariable = condition ? (int?) null : 100;

In this case, as the value is null, we need it to be a reference (or nullable) type. This proposal makes the compiler somewhat smarter, in that it will be able to guess what we want and declare myVariable to be nullable.

Declaration expressions

It shall be possible to declare variables in a method call expression and use them in the same scope:

if (M((var x = expr).a, x.b) { }

Dictionary literals

For dictionaries (Dictionary<TKey, TValue>) we will have a special syntax:

var x = ["foo": 4, "bar": 5];

Yes, it’s true that we already had a syntax for any collection that offered an Add method, but this one is very specific to dictionaries, and it’s easier to understand.

UTF-8 string literals

.NET’s strings (the String class) use UTF-16 encoding. This proposal will make it possible to use UTF-8 for constants and maybe some APIs will have overloads to support it. The main reason for this is space, e.g., a UTF-8 take half the size of its UTF-16 counterpart. Just beware storing national characters in it, as most will not be supported.

Pattern-based ‘with’ expressions

We already had this in VB and JavaScript, now we will have it in C#: the with expression will somewhat simplify assigning values to fields or properties of some variable. The proposed syntax is like this:

with (point) 
{ 
    x = 10; 
    y = 20; 
}

Type classes

Type classes, shapes or concepts exist in several languages and this proposal aims to bring them to C# as well. It will make it possible to define operations and operators for generic types that match a given constraint. This can be regarded as a general-purpose mechanism for adding extensions (not just methods) to classes. The actual syntax may change, but here is a simple example:

public shape SNumber<T> 
{ 
    static T operator + (T t1, T t2); 
    static T Zero { get; } 
    static implicit operator bool (T t1); 
} 
 
public extension IntGroup of int : SNumber 
{ 
    public static int Zero => 0; 
    public static int operator + (int t1, int t2) => t1 + t2; // this one is actually inferred automatically 
    public static implicit operator bool (int t1) => t1 != 0; 
}

This example shows a shape (SNumber<T>) and an actual extension of it applied to type int. The shape introduced the concepts of zero, addition, and conversion to boolean, and the extension applied them for the integer type.

Default interface methods

This originated from Java is is likely to raise some controversy. As you know, unlike abstract classes, interfaces cannot have constructor, method, property or field declarations; adding a method declaration to an interface breaks existing code, as they do not have an implementation for it. This proposal allows adding methods with a default implementation to existing interfaces, and these methods are treated as virtual, which means that implementing classes can change the implementation. It’s true that we already had extension methods, but these are not really part of the type they’re extending, which means that we can’t list them using reflection, for example. Imagine we had an interface like this:

public interface IMyService 
{ 
}

Later on we could extend it by having it implement IDisposable, but then we would need to provide a default implementation for its Dispose method:

public interface IMyService : IDisposable 
{ 
    public void Dispose() { /*do nothing by default*/ } 
}

As you can see, these are just method declarations with a body, like we have in regular types. The Dispose method can be overridden in implementing classes.

Allow no-arguments constructor and field initializers instruct declarations

Up until now, if we had declared a constructor in a struct, we had to initialize every field/property members from it. Also, we could not initialize field/property members in their declarations, e.g., this wasn’t valid C# code:

public struct Complex 
{ 
    public Complex() 
    { 
    } 
 
    public double Img { get; set; } = 0; 
    public double Real { get; set; } = 0; 
}

Some of you will be happy to know that this is now possible, which brings structs in sync with classes.

Records

C# 8 will introduce a more concise syntax for defining types with just properties (records):

class Point(int X, int Y)

This will result in a class with properties X and Y of the int type. Moreover, the compiler will implement IEquatable<T> and the Equals and GetHashCode methods for us. Essentially, a simplified declaration, the record type will behave just like any other type.

Nullable reference types

Tony Hoare invented the null concept back in the 60’s. After that, he famously called it his “billion-dollar mistake”. Why is that? Well, null references do lead to a big number of crashes. How would you feel about having a programming language without nulls?

This is probably the most debatable new feature in C# 8. The idea is that, like already happens with value types, reference types cannot have the null value by design. If you wish to have a reference type variable assigned to null, you will have to declare it in a different way explicitly. The way to mark a reference type as being nullable is the same as for value types, with the ? character.

So, this won’t be allowed:

MyReferenceType builder = null;

You will need to use this instead:

MyReferenceType? builder = null;

This will apply only to new reference types, declared in your own assemblies; types declared in external references will continue to work the same way, this is so that your code still compiles.

Keep in mind that this will be an opt-in decision, that is, you may continue with the old behavior of allowing nullable reference types by default.

Microsoft believes that this will result in better code, with less bugs. At least, we can give it a try!

Async streams

This one is cool, we will be able to write code like this:

foreach (await var item in ae.GetEnumerator(cancellationToken)) { ... }

There will be new interfaces (and matching types) for asynchronous traversal of lists (IAsyncEnumerable<T>, IAsyncEnumerator<T>) and also asynchronous disposal (IAsyncDisposable).

Conclusion

As you can see, C# evolved quite a bit and some of its new features would seem totally unbelievable just a couple years ago. In general, we’re talking about improvements to the language, we have not constrained any way to use them, but here and there they do offer interesting new options. I guess time will tell what exactly is useful and what isn’t, but, in the meantime, we may as well get used to them. As you can see, quite a lot of them are related with performance and unmanaged/unsafe code, which goes to say, they are not targeted at the average developer. Oh, and by the way, until the features are closed, you can make yourself heard: do share your thoughts with the Microsoft team, you can make a difference!

With APM, server health metrics, and error log integration, improve your application performance with Stackify Retrace.  Try your free two week trial today

References

What’s new in C# 7

What’s new in C# 7.1

What’s new in C# 7.2

C# 7.3 Proposal

C# Language 8.0 Milestone

Language Feature Status

C# Language Design

Visual Studio Roadmap

Pre-release Visual Studio 2017

3 New C# 8 Features We Are Excited About

The .NET Ecosystem: Dive Into Runtime Tools and Languages

The Java vs .NET Comparison (Explained with Cats)

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]