What is the difference between below two implementations?
Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid(); //1st statement
// public Guid Id => Guid.NewGuid(); //2nd statement
The below lines of code behaving differently for Singleton
For the above case (2nd statement is commented out) the code works correctly. One can find out by looking at same Guid for all the requests.
But if one runs by making following changes. The Guid is different for all the requests.
//Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid(); //1st statement
public Guid Id => Guid.NewGuid(); //2nd statement
IReportServiceLifetime.cs
public interface IReportServiceLifetime
{
Guid Id { get; }
ServiceLifetime Lifetime { get; }
}
IExampleSingletonService.cs
public interface IExampleSingletonService : IReportServiceLifetime
{
ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Singleton;
}
ExampleSingletonService.cs
internal sealed class ExampleSingletonService : IExampleSingletonService
{
Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
// public Guid Id => Guid.NewGuid();
}
ServiceLifetimeReporter.cs
internal sealed class ServiceLifetimeReporter
{
private readonly IExampleSingletonService _singletonService;
public ServiceLifetimeReporter(IExampleSingletonService singletonService) =>
(_singletonService) =
(singletonService);
public void ReportServiceLifetimeDetails(string lifetimeDetails)
{
Console.WriteLine(lifetimeDetails);
LogService(_singletonService, "Always the same");
}
private static void LogService<T>(T service, string message)
where T : IReportServiceLifetime =>
Console.WriteLine(
$" {typeof(T).Name}: {service.Id} ({message})");
}
Program.cs
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddSingleton<IExampleSingletonService, ExampleSingletonService>();
services.AddTransient<ServiceLifetimeReporter>();
})
.Build();
ServiceLifetime(host.Services, "Lifetime 1");
ServiceLifetime(host.Services, "Lifetime 2");
await host.RunAsync();
static void ServiceLifetime(IServiceProvider hostProvider, string lifetime)
{
using IServiceScope serviceScope = hostProvider.CreateScope();
IServiceProvider provider = serviceScope.ServiceProvider;
ServiceLifetimeReporter logger = provider.GetRequiredService<ServiceLifetimeReporter>();
logger.ReportServiceLifetimeDetails(
$"{lifetime}: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()");
Console.WriteLine("...");
logger = provider.GetRequiredService<ServiceLifetimeReporter>();
logger.ReportServiceLifetimeDetails(
$"{lifetime}: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()");
Console.WriteLine();
}
What you wrote is a simplified syntax for properties that is often called "syntactic sugar". The C# compiler expands this:
public Guid Id1 { get; } = Guid.NewGuid();
public Guid Id2 => Guid.NewGuid();
... to a code equivalent to:
private readonly Guid _Id1_BackingField = Guid.NewGuid();
public Guid Id1
{
get
{
return _Id1_BackingField;
}
}
public Guid Id2
{
get
{
return Guid.NewGuid();
}
}
The backing field is however hidden.
The creation expression of Id
is converted to a field initializer of a read-only backing field.
Whereas for Id2
the creation expression is used in a return-statement which is obviously called every time you are reading the property.
You can test this on SharpLab.io. The trick the C# compiler uses to hide the backing field is to use an identifier which is invalid in C# (<Id1>k__BackingField
).
Note that the C# compiler generates IL code. This identifier is valid in IL. (You can switch to IL view through the drop-down-box on the right panel.)