Skip to content

Instantly share code, notes, and snippets.

@rstropek
Last active November 30, 2020 11:37
Show Gist options
  • Save rstropek/1430b24436f948f298b7d63a72896b4f to your computer and use it in GitHub Desktop.
Save rstropek/1430b24436f948f298b7d63a72896b4f to your computer and use it in GitHub Desktop.
C# Records
var heroes = new Hero[] {
new("Homelander", "DC", true),
new("Jessica Jones", "Marvel", false),
};
class Hero {
public Hero(string name, string universe, bool canFly) =>
(Name, Universe, CanFly) = (name, universe, canFly);
public string Name { get; }
public string Universe { get; }
public bool CanFly { get; }
}
var heroes = new Hero[] {
// Note auto-generated constructor
new("Homelander", "DC", true),
new("Jessica Jones", "Marvel"),
};
// Note auto-generated deconstructor
var (name, universe, canFly) = heroes[0];
System.Console.WriteLine(name);
record Hero(string Name, string Universe, bool CanFly = false);
var heroes = new Hero[] {
new("Homelander", "DC", true),
new("Groot", "Marvel", false),
new RealLifeHero("Superman", "DC", true, "Clark", "Kent"),
// Next object uses alternative constructor
new RealLifeHero("Jessica", "Jones", "Marvel", false),
};
interface IHero
{
string Name { get; }
string Universe { get; }
}
// Records can implement interfaces
record Hero(string Name, string Universe, bool CanFly) : IHero
{
// Records can have methods just like regular classes
public override string ToString() =>
$"{Name} ({Universe})";
}
// Records can have base records
record RealLifeHero(string Name, string Universe, bool CanFly,
string RealLifeFirstName, string RealListLastName)
: Hero(Name, Universe, CanFly)
{
// Records can have additional members
public string FullName => $"{RealLifeFirstName} {RealListLastName}";
// Records can have additional constructors that call the
// constructor generated for the record.
public RealLifeHero(string firstName, string lastName,
string universe, bool canFly) : this($"{firstName} {lastName}",
universe, canFly, firstName, lastName)
{ }
}
using System.Collections.Immutable;
Hero homelander;
var heroes = new Hero[] {
homelander = new("Homelander", "DC", true),
// Create copy of homelander with changed name
homelander with { Name = "Stormfont" },
new("Groot", "Marvel", false),
};
// Fix typo in hero at index 1. We need to create
// a copy of the object to achieve that.
heroes[1] = heroes[1] with { Name = "Stormfront" };
// Records work very well in conjunction with immutable collections.
var immutableArray = ImmutableArray<Hero>.Empty;
immutableArray = immutableArray.Add(homelander = new("Homelander", "DC", true));
immutableArray = immutableArray.Add(homelander with { Name = "Stormfont" });
immutableArray = immutableArray.SetItem(1, immutableArray[1] with
{ Name = "Stormfront" });
record Hero(string Name, string Universe, bool CanFly);
Hero homelander;
var heroes = new Hero[] {
// Assign value to init-only property
homelander = new("Homelander", "DC") { CanFly = true },
homelander with { Name = "The Deep" },
new("Groot", "Marvel"),
};
// Change init-only property using `with`
heroes[1] = heroes[1] with { CanFly = false };
record Hero(string Name, string Universe)
{
public bool CanFly { get; init; } = false;
}
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
var heroes = new Hero[] {
new("Homelander", "DC", true),
new("Jessica Jones", "Marvel"),
};
foreach (var h in heroes)
{
var json = JsonSerializer.Serialize(h);
// Will lead to e.g. `{"hn":"Homelander","univ":"DC"}`
Console.WriteLine(json);
}
record Hero(string Name, string Universe, bool CanFly = false)
{
[JsonPropertyName("hn")]
public string Name { get; init; } = Name;
[JsonPropertyName("univ")]
public string Universe { get; init; } = Universe;
[JsonIgnore]
public bool CanFly { get; init; } = CanFly;
}
using static System.Console;
var hero1 = new Hero("Homelander", "DC", true);
var hero2 = hero1;
var hero3 = new Hero("Homelander", "DC", true);
// Obviously, hero1 and hero2 are equal.
if (hero1 == hero2) WriteLine("Hero 1 and 2 are equal");
// hero1 and hero3 are also equal, because properties
// are compared, not object references.
if (hero1 == hero3) WriteLine("Hero 1 and 3 are equal, too");
var hero4 = new RealLifeHero("Superman", "DC", true)
{ RealLifeFirstName = "Clark", RealLifeLastName = "Kent" };
var hero5 = new RealLifeHero("Superman", "DC", true)
{ RealLifeFirstName = "Foo", RealLifeLastName = "Bar" };
// hero4 and hero5 are NOT equal, because values of additional
// init-only properties are complared, too.
if (hero4 == hero5) WriteLine("Hero 4 and 5 are equal, too");
record Hero(string Name, string Universe, bool CanFly = false);
record RealLifeHero(string Name, string Universe, bool CanFly)
: Hero(Name, Universe, CanFly)
{
// Note that RealLifeHero has two additional init-only properties.
// We want to explore whether they are compared, too.
public string RealLifeFirstName { get; init; }
public string RealLifeLastName { get; init; }
}
// Reference AutoMapper NuGet package for this sample
using AutoMapper;
using static System.Console;
var homelander = new Hero("Homelander", "DC", true);
var traditionalHomelander = new TraditionalHero
{ Name = "Homelander", Universe = "DC", CanFly = true };
// Use AutoMapper to convert record into traditional
// class and vice versa.
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Hero, TraditionalHero>();
cfg.CreateMap<TraditionalHero, Hero>();
});
var mapper = config.CreateMapper();
// Record into class
var traditionalHero = mapper.Map<TraditionalHero>(homelander);
WriteLine(traditionalHero.Name);
// Class into record
var hero = mapper.Map<Hero>(traditionalHomelander);
WriteLine(hero.Name);
record Hero(string Name, string Universe, bool CanFly = false);
class TraditionalHero
{
public string Name { get; set; }
public string Universe { get; set; }
public bool CanFly { get; set; }
}
// You need Entity Framework Core 5.0 and
// SQL LocalDB for this sample.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using System.Linq;
using static System.Console;
var factory = new HeroContextFactory();
using var context = factory.CreateDbContext(null);
// Add a record to the database.
var hero = new Hero(0, "Homelander", "DC", true);
context.Heroes.Add(hero);
await context.SaveChangesAsync();
// Working with relations works, too
var team = new Team(0, "The Seven")
{ Heroes = new() { hero, new Hero(0, "The Deep", "DC", false) } };
context.Teams.Add(team);
await context.SaveChangesAsync();
// Note that the ID will be set by EFCore, but NOT
// using the clone/`with` mechanism. The ID is changed
// in the existing object -> breaks immutability rule.
WriteLine($"The ID of the new row in the DB is {hero.ID}");
// If we want to update an object, we have to detach
// it first because we must create a copy of the record
// and attach the copy instead of the original object.
context.Entry(hero).State = EntityState.Detached;
hero = hero with { Name = "Stormfront" };
context.Heroes.Update(hero);
await context.SaveChangesAsync();
// Querying with records works just fine.
var theSeven = await context.Teams
.Include(t => t.Heroes)
.FirstAsync(t => t.Name == "The Seven");
WriteLine(string.Join(',', theSeven.Heroes.Select(h => h.ID)));
record Team(int ID, string Name)
{
public List<Hero> Heroes { get; init; } = new();
}
record Hero(int ID, string Name, string Universe, bool CanFly);
class HeroContext : DbContext
{
public HeroContext(DbContextOptions<HeroContext> options)
: base(options)
{ }
// A record is just a special form of a class, so we
// can use it with Entity Framework.
public DbSet<Hero> Heroes { get; set; }
public DbSet<Team> Teams { get; set; }
}
class HeroContextFactory : IDesignTimeDbContextFactory<HeroContext>
{
public HeroContext CreateDbContext(string[] args)
{
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
var optionsBuilder = new DbContextOptionsBuilder<HeroContext>();
optionsBuilder.UseSqlServer(configuration["ConnectionStrings:DefaultConnection"]);
return new HeroContext(optionsBuilder.Options);
}
}
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.ConfigureServices(services => services.AddControllers())
.Configure(app =>
{
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());
});
})
.Build()
.Run();
[Route("api/[controller]")]
[ApiController]
public class HeroesController : ControllerBase
{
public record HeroDto(int ID, string Name, string Universe, bool CanFly);
private List<HeroDto> Heroes { get; } = new()
{
new(1, "Homelander", "DC", true),
new(2, "Groot", "Marvel", false),
};
[HttpGet]
[Route("")]
public IActionResult GetAllHeroes() => Ok(Heroes);
[HttpGet("{id}", Name = nameof(GetHeroById))]
public IActionResult GetHeroById(int id) =>
Heroes.FirstOrDefault(h => h.ID == id) switch
{
null => NotFound(),
var h => Ok(h)
};
[HttpPost]
public IActionResult AddHero([FromBody] HeroDto hero)
{
Heroes.Add(hero);
return CreatedAtRoute(nameof(GetHeroById), new { id = hero.ID }, hero);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment