.NET 6 Release Candidate 1 (RC1) is now available and includes many great new improvements to ASP.NET Core.
Here’s what’s new in this preview release:
- Render Blazor components from JavaScript
- Blazor custom elements
- Manipulate the query string from Blazor
- .NET to JavaScript streaming
PageX
andPageY
inMouseEventArgs
- Blazor templates updated to set page titles
- Disabled long-polling transport for Blazor Server
- Collocate JavaScript files with pages, views, and components
- JavaScript initializers
- Customize Blazor WebAssembly packaging
- Template improvements
- Minimal API updates
- Support for Latin1 encoded request headers in
HttpSysServer
- Emit
KestrelServerOptions
viaEventSource
event - Add timestamps and PID to ASP.NET Core Module logs
- New
DiagnosticSource
event for rejected HTTP requests - Create a
ConnectionContext
from an Accept Socket - Streamlined HTTP/3 setup
- Upgrade to Duende Identity Server
Get started
To get started with ASP.NET Core in .NET 6 RC1, install the .NET 6 SDK.
If you’re on Windows using Visual Studio, install the latest preview of Visual Studio 2022. Support for .NET 6 in Visual Studio for Mac is coming soon, and is currently available as a private preview.
To get setup with .NET MAUI & Blazor for cross-platform native apps, see the latest instructions in the .NET MAUI getting started guide. Please also read this important update on .NET MAUI.
To install the .NET WebAssembly build tools, run the following command from an elevated command prompt:
dotnet workload install wasm-tools
Alternative, use the Visual Studio Installer to enable the “.NET WebAssembly build tools” optional component in the “ASP.NET and web development” workload.
Upgrade an existing project
To upgrade an existing ASP.NET Core app from .NET 6 Preview 7 to .NET 6 RC1:
- Update all Microsoft.AspNetCore.* package references to
6.0.0-rc.1.*
. - Update all Microsoft.Extensions.* package references to
6.0.0-rc.1.*
.
To upgrade a .NET MAUI Blazor app from .NET 6 Preview 7 to .NET 6 RC1 we recommend starting from a new .NET MAUI Blazor project created with the .NET 6 RC1 SDK and then copying code over from your original project.
See the full list of breaking changes in ASP.NET Core for .NET 6.
Render Blazor components from JavaScript
Blazor is great for building client-side web UI using just .NET, but what if you already have existing JavaScript apps that you need to maintain and evolve? How can you avoid having to build common components twice, in .NET and JavaScript?
In .NET 6, you can now dynamically render Blazor components from JavaScript. This capability enables you to integrate Blazor components with existing JavaScript apps.
To render a Blazor component from JavaScript, first register it as a root component for JavaScript rendering and assign it an identifier:
Blazor Server
builder.Services.AddServerSideBlazor(options =>
{
options.RootComponents.RegisterForJavaScript<Counter>(identifier: "counter");
});
Blazor WebAssembly
builder.RootComponents.RegisterForJavaScript<Counter>(identifier: "counter");
Load Blazor into your JavaScript app (blazor.server.js or blazor.webassembly.js) and then render the component from JavaScript into a container element using the registered identifier, passing component parameters as needed:
let containerElement = document.getElementById('my-counter');
await Blazor.rootComponents.add(containerElement, 'counter', { incrementAmount: 10 });
Blazor custom elements
Experimental support is also now available for building custom elements with Blazor using the Microsoft.AspNetCore.Components.CustomElements NuGet package. Custom elements use standard HTML interfaces to implement custom HTML elements.
To create a custom element using Blazor, register a Blazor root component as custom elements like this:
options.RootComponents.RegisterAsCustomElement<Counter>("my-counter");
You can then use this custom element with any other web framework you’d like. For example, here’s how you would use this Blazor counter custom element in a React app:
<my-counter increment-amount={incrementAmount}></my-counter>
See the Blazor Custom Elements sample project for a complete example of how to create custom elements with Blazor.
This feature is experimental because we’re still working out some of the details for how best to support custom elements with Blazor. We welcome your feedback on how well this particular approach meets your requirements.
Generate Angular and React components using Blazor
You can also now generate framework specific JavaScript components from Blazor components for frameworks like Angular or React. This capability isn’t included with .NET 6, but is enabled by the new support for rendering Blazor components from JavaScript. The JavaScript component generation sample on GitHub demonstrates how you can generate Angular and React components from Blazor components.
In this sample, you attribute Blazor components to generate Angular or React component wrappers:
@*Generate an Angular component*@
@attribute [GenerateAngular]
@*Generate an React component*@
@attribute [GenerateReact]
You then register the Blazor components as Angular or React components:
options.RootComponents.RegisterForAngular<Counter>();
options.RootComponents.RegisterForReact<Counter>();
When the project gets built, it generates Angular and React components based on your Blazor components. You then use the generated Angular and React components like you would normally:
// Angular
<counter [incrementAmount]="incrementAmount"></counter>
// React
<Counter incrementAmount={incrementAmount}></Counter>
This sample isn’t a complete solution for generating Angular and React components from Blazor components, but we hope it demonstrates what’s possible. We welcome and encourage community efforts to build on this functionality more fully. We’re excited to see what the community does with this feature!
Manipulate the query string from Blazor
New GetUriWithQueryParameter
and GetUriWithQueryParameters
extension methods on NavigationManager
facilitate updating the query string of the browser URL.
To add or update a single query string parameter:
// Create a new URI based on the current address
// with the specified query string parameter added or updated.
var newUri = NavigationManager.GetUriWithQueryParameter("page", 3);
// Navigate to the new URI with the updated query string
NavigationManager.NavigateTo(newUri);
Use GetUriWithQueryParameters
to add, update, or remove multiple parameters based on a dictionary of parameter values (a null
value removes the parameter).
.NET to JavaScript streaming
Blazor now supports streaming data from .NET to JavaScript. A .NET stream can be passed to JavaScript as a DotNetStreamReference
.
using var data = new System.IO.MemoryStream(new byte[100 * 1024]);
using var streamRef = new DotNetStreamReference(stream: data, leaveOpen: false);
await JS.InvokeVoidAsync("consumeStream", streamRef);
In JavaScript, the data stream can then be read as an array buffer or as a readable stream:
async function consumeStream(streamRef) {
const data = await streamRef.arrayBuffer(); // ArrayBuffer
// or
const stream = await streamRef.stream(); // ReadableStream
}
Additional details on this feature are available in the Blazor JavaScript interop docs.
PageX
and PageY
in MouseEventArgs
MouseEventArgs
now has PageX
and PageY
properties corresponding to the standard pagex and pagey on the MouseEvent
interface. Thank you TonyLugg for helping us fill this functional gap.
Blazor templates updated to set page title
The Blazor project templates have been updated to support updating the page title as the user navigates to different pages using the new PageTitle
and HeadOutlet
components.
In the Blazor WebAssembly template, the HeadOutlet
component is added as a root component that appends to the HTML head tag. Each page in the template sets the title using the PageTitle
component.
The Blazor Server template required a bit more refactoring in order to support modifying the head when prerendering. The _Host.cshtml page now has its own layout, _Layout.cshtml, that adds the HeadOutlet
component to the HTML head using the component tag helper. This ensures that the HeadOutlet
is rendered before any components that want to modify the head.
Disabled long-polling transport for Blazor Server
Prior to .NET 6, Blazor Server apps would fall back to long-polling when WebSockets weren’t available, which often led to a degraded user experience. In .NET 6 we’ve disabled the long-polling transport for Blazor Server apps by default so that it’s easier to know when WebSockets haven’t been correctly configured. If your Blazor Server app still requires support for long-polling, you can reenable it. See the related breaking change notification for details.
Collocate JavaScript files with pages, views, and components
You can now collocate a JavaScript file with pages, views, and components using the .cshtml.js and .razor.js conventions. This is a convenient way to organize your code when you have JavaScript code that is specific to a page, view, or component. These files are publicly addressable using the path to the file in the project (Pages/Index.cshtml.js
or _content/{LIBRARY NAME}/Pages/Index.cshtml.js
if the file is coming from a library).
For example, if you have a component implemented by Pages/Counter.razor, you can add a JavaScript module for that component at Pages/Counter.razor.js and then load it like this:
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./Pages/Counter.razor.js");
JavaScript initializers
JavaScript initializers provide a way to execute some logic before and after a Blazor app loads. This is useful for customizing how a Blazor app loads, initializing libraries before Blazor starts up, and configuring Blazor settings.
To define a JavaScript initializer, add a JavaScript module to the web root of your project named {LIBRARY NAME}.lib.module.js. Your module can export the following well-known functions:
beforeStart
: Called before Blazor boots up on the .NET side. Used to customize the loading process, logging level, and other hosting model specific options.- In Blazor WebAssembly,
beforeStart
receives the Blazor WebAssembly options and any extensions added during publishing. - In Blazor Server,
beforeStart
receives the circuit start options. - In BlazorWebViews, no options are passed.
- In Blazor WebAssembly,
afterStarted
: Called after Blazor is ready to receive calls from JavaScript. Used to initialize libraries by making .NET interop calls, registering custom elements, etc.- The
Blazor
instance is always passed toafterStarted
as an argument.
- The
A basic JavaScript initializer looks like this:
RazorClassLibrary1.lib.module.js
export function beforeStart(options) {
console.log("beforeStart");
}
export function afterStarted(blazor) {
console.log("afterStarted");
}
JavaScript initializers are detected as part of the build and then imported automatically in Blazor apps. This removes the need in many cases for manually adding script references when using Blazor libraries.
Customize Blazor WebAssembly packaging
In some environments, firewalls or other security software block the download of .NET assemblies, which prevents the execution of Blazor WebAssembly apps. To enable support for Blazor WebAssembly in these environments, we’ve made the publishing and loading process for Blazor WebAssembly apps extensible so that you can customize the packaging and loading of the app. We’ll share more details on how to do this in a future post.
Template improvements
Implicit usings
Based on feedback from Preview 7, changes to the implicit usings feature were made as part of this release, including the requirement to opt-in to implicit usings in the project file, rather than them being included by default based on the project targeting .NET 6. This will ensure existing projects being migrated to .NET 6 aren’t impacted by the implicit usings until the author is ready to enable the feature.
You can read more about this change in its breaking changes document.
Randomized port allocation
New ASP.NET Core projects will now have random ports assigned during project creation for use by the Kestrel web server, matching the existing behavior when using IIS Express. This helps to minimize the chance that new projects end up using ports that are already in use on the machine, which results in a failure of the app to start when it’s launched from Visual Studio, or via dotnet run
.
A port from 5000-5300 will be selected for HTTP, and from 7000-7300 for HTTPS, at the time the project is created. As always, the ports used during development can be easily changed by editing the project’s launchSettings.json file. When the app is run after publishing, Kestrel still defaults to using ports 5000 and 5001 (for HTTP and HTTPS respectively) if not otherwise configured. You can read more about configuring Kestrel in the docs.
New logging defaults
New ASP.NET Core projects have a simplified logging configuration in their appsettings.json and appsettings.Development.json files.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
The net effect of this change is that log messages at the “Information” level from sources other than ASP.NET Core, will now be emitted by default. This includes messages related to relational database querying from Entity Framework Core, which are now clearly visible by default in new applications:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7297
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5131
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Users\MyName\source\repos\WebApplication45\WebApplication45
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (25ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [w].[Id], [w].[Name]
FROM [Widget] AS [w]
Minimal API updates
Improved support for OpenAPI
In RC1, we improved support for OpenAPI by adding ways to define metadata on a minimal endpoint either imperatively via extensions methods or declaratively via attributes.
We added support for the following metadata:
WithName
metadata maps the endpoint name to an operationId in generated OpenAPI documents.- Note the endpoint name is also used when using
LinkGenerator
to generate URL/links for endpoints - When the user does not specify the endpoint name using the
WithName
metadata, the operation id will default to the name of the function (SayHello
) as shown in the following example:
- Note the endpoint name is also used when using
string SayHello(string name) => $"Hello, {name}!";
app.MapGet("/hello/{name}", SayHello);
WithGroupName
metadata maps the endpoint group name to the document name in generated OpenAPI documents (typically used for versioning, e.g. “v1”, “v2”)ExcludeFromDescription
metadata indicates that the API should be excluded/ignored from OpenAPI document generationProducesValidationProblem
indicates that the endpoint will produce 4xx http status codes and error details withapplication/validationproblem+json
content typeProduces<T>(...)
metadata indicates what response types a method produces, where each response is the combination of:- One HTTP status code
- One or more content types, e.g. “application/json”
- An optional schema per content type
Examples
OpenAPI extension methods can be used to imperatively add the required metadata to the endpoint:
app.MapGet("/admin", () => "For admins only")
.WithName("AdminDetails")
.RequiresAuthorization("Admins")
.ExcludeFromDescription();
app.MapPost("/todos", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.CreatedAtRoute("GetTodoById", new { todo.Id }, todo);
})
.WithName("AddTodo")
.WithGroupName("v1")
.ProducesValidationProblem()
.Produces<Todo>(StatusCodes.Status201Created);
In addition to extension methods, attributes can be used to add metadata declaratively on endpoints:
app.MapGet("/admin", AdminDetails);
app.MapPost("/todos", AddTodo);
[Authorized(Policy = "Admins")]
[ExcludeFromDescription]
string AdminDetails()
{
return "For admins only";
}
[EndpointGroupName("v1")]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest, "application/problem+json")]
[ProducesResponseType(typeof(Todo), StatusCodes.Status201Created)]
async Task<IResult> AddTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todos/{todo.Id}", todo);
}
Parameter Binding improvements
Allow optional parameters in endpoint actions
We introduced support to allow developers to specify if parameters in their minimal action are optional.
For example, we can designate name
as an optional route parameter in the sample below:
app.MapGet("/sayHello/{name?}", (string? name) => $"Hello World from {name}");
This indicates that both of the following calls will succeed by returning 2xx status code since the name
parameter is optional.
curl localhost:5000/sayHello/John
and curl localhost:5000/sayHello
.
The same behavior applies for request bodies. The following example will call the method with a null
todo when there is no a request body sent in a post/put call:
app.MapPost("/todos", (Todo? todo) => () => { });
Route parameters, query parameters, and body parameters can all be designated as optional by using a nullability annotation or providing a default value. When a required parameter is not provided, the delegate will return a 400 status.
As part of the changes, we also improved error messages when parameters fail to bind.
Fix issue with struct
support in parameter binding
In Preview 7, binding a struct
parameter did not work and would throw an System.InvalidOperationException: The binary operator Equal is not defined for the types 'Contact' and 'System.Object'
. In RC1, we fixed the issue.
Now it’s possible for developers to write the code below for a minimal API:
var app = WebApplication.Create(args);
app.MapPost("/api/contact", (Contact contact) => $"{contact.PhoneNumber} {contact.Email}");
app.Run();
record struct Contact(string PhoneNumber, string Email);
Developer Exception Page Middleware change
With RC1, the DeveloperExceptionPageMiddleware
will now be registered as the first middleware if the current environment is development. This means when IWebHostEnvironment.IsDevelopment()
is true. This removes the need to manually register the middleware by developers as shown in the example below.
Before
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapGet("/", "Hello World");
app.Run();
After
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", "Hello World");
app.Run();
Other Improvements in minimal APIs
Modifying the host configuration after the WebApplicationBuilder
has been created is no longer supported. Instead, the Create
and CreateBuilder
calls now support taking a WebApplicationBuilderOptions
which can be used to specify properties like the environment name, content root path, and so on.
var config = new WebApplicationOptions
{
Args = args,
EnvironmentName = Environments.Staging,
ContentRootPath = "www/"
};
var app = WebApplication.Create(options);
app.MapGet("/", (IHostEnvironment env) => env.EnvironmentName);
app.Run();
In addition to configuring hosting settings using the WebApplicationOptions
class, minimal APIs now support customizing the host configuration using command-line args. For example, the following can be used to set the environment name for a running app.
$ dotnet run --environment=Development
To support a wider variety of middleware, minimal API apps now support multiple calls to the UseRouting
method without overriding existing endpoints. This enables registering middleware like the Developer Exception page (which is enabled by default now) in the app.
Minimal APIs now support using MapFallback
to define behavior for fallback routes in apps.
var app = WebApplication.Create(options);
app.MapFallback("/subroute", (string shareCode) => { ... };
app.Run();
Support for Latin1 encoded request headers in HttpSysServer
HttpSysServer
is now capable of decoding request headers that are Latin1 encoded. To do so, you must the set the UseLatin1RequestHeaders
property on HttpSysOptions
to true
.
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseHttpSys(o => o.UseLatin1RequestHeaders = true);
Emit KestrelServerOptions
via EventSource
event
The KestrelEventSource
now emits a new event containing the JSON-serialized KestrelServerOptions
when enabled (with verbosity EventLevel.LogAlways
). This event makes it easier to reason about the server behavior when analyzing collected traces. Here’s an example of the event payload:
{
"AllowSynchronousIO": false,
"AddServerHeader": true,
"AllowAlternateSchemes": false,
"AllowResponseHeaderCompression": true,
"EnableAltSvc": false,
"IsDevCertLoaded": true,
"RequestHeaderEncodingSelector": "default",
"ResponseHeaderEncodingSelector": "default",
"Limits": {
"KeepAliveTimeout": "00:02:10",
"MaxConcurrentConnections": null,
"MaxConcurrentUpgradedConnections": null,
"MaxRequestBodySize": 30000000,
"MaxRequestBufferSize": 1048576,
"MaxRequestHeaderCount": 100,
"MaxRequestHeadersTotalSize": 32768,
"MaxRequestLineSize": 8192,
"MaxResponseBufferSize": 65536,
"MinRequestBodyDataRate": "Bytes per second: 240, Grace Period: 00:00:05",
"MinResponseDataRate": "Bytes per second: 240, Grace Period: 00:00:05",
"RequestHeadersTimeout": "00:00:30",
"Http2": {
"MaxStreamsPerConnection": 100,
"HeaderTableSize": 4096,
"MaxFrameSize": 16384,
"MaxRequestHeaderFieldSize": 16384,
"InitialConnectionWindowSize": 131072,
"InitialStreamWindowSize": 98304,
"KeepAlivePingDelay": "10675199.02:48:05.4775807",
"KeepAlivePingTimeout": "00:00:20"
},
"Http3": {
"HeaderTableSize": 0,
"MaxRequestHeaderFieldSize": 16384
}
},
"ListenOptions": [
{
"Address": "https://127.0.0.1:7030",
"IsTls": true,
"Protocols": "Http1AndHttp2"
},
{
"Address": "https://[::1]:7030",
"IsTls": true,
"Protocols": "Http1AndHttp2"
},
{
"Address": "http://127.0.0.1:5030",
"IsTls": false,
"Protocols": "Http1AndHttp2"
},
{
"Address": "http://[::1]:5030",
"IsTls": false,
"Protocols": "Http1AndHttp2"
}
]
}
Add timestamps and PID to ASP.NET Core Module logs
The ASP.NET Core Module (ANCM) enhanced diagnostic logs now include timestamps and PID of the process emitting the logs. This makes it easier to diagnose issues with overlapping process restarts in IIS when you may have multiple IIS worker processes running.
The resulting logs now resemble the sample output included below:
[2021-07-28T19:23:44.076Z, PID: 11020] [aspnetcorev2.dll] Initializing logs for 'C:\<path>\aspnetcorev2.dll'. Process Id: 11020. File Version: 16.0.21209.0. Description: IIS ASP.NET Core Module V2. Commit: 96475a2acdf50d7599ba8e96583fa73efbe27912.
[2021-07-28T19:23:44.079Z, PID: 11020] [aspnetcorev2.dll] Resolving hostfxr parameters for application: '.\InProcessWebSite.exe' arguments: '' path: 'C:\Temp\e86ac4e9ced24bb6bacf1a9415e70753\'
[2021-07-28T19:23:44.080Z, PID: 11020] [aspnetcorev2.dll] Known dotnet.exe location: ''
New DiagnosticSource
event for rejected HTTP requests
Kestrel now emits a new DiagnosticSource
event for HTTP requests rejected at the server layer. Prior to this change, there was no way to observe these rejected requests. The new DiagnosticSource event Microsoft.AspNetCore.Server.Kestrel.BadRequest
now contains a IBadRequestExceptionFeature
that can be used to introspect the reason for rejecting the request.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var diagnosticSource = app.Services.GetRequiredService<DiagnosticListener>();
using var badRequestListener = new BadRequestEventListener(diagnosticSource, (badRequestExceptionFeature) =>
{
app.Logger.LogError(badRequestExceptionFeature.Error, "Bad request received");
});
app.MapGet("/", () => "Hello world");
app.Run();
class BadRequestEventListener : IObserver<KeyValuePair<string, object>>, IDisposable
{
private readonly IDisposable _subscription;
private readonly Action<IBadRequestExceptionFeature> _callback;
public BadRequestEventListener(DiagnosticListener diagnosticListener, Action<IBadRequestExceptionFeature> callback)
{
_subscription = diagnosticListener.Subscribe(this!, IsEnabled);
_callback = callback;
}
private static readonly Predicate<string> IsEnabled = (provider) => provider switch
{
"Microsoft.AspNetCore.Server.Kestrel.BadRequest" => true,
_ => false
};
public void OnNext(KeyValuePair<string, object> pair)
{
if (pair.Value is IFeatureCollection featureCollection)
{
var badRequestFeature = featureCollection.Get<IBadRequestExceptionFeature>();
if (badRequestFeature is not null)
{
_callback(badRequestFeature);
}
}
}
public void OnError(Exception error) { }
public void OnCompleted() { }
public virtual void Dispose() => _subscription.Dispose();
}
Create a ConnectionContext
from an Accept Socket
The newly introduced SocketConnectionContextFactory
now makes it possible to create a ConnectionContext
from an already accepted socket. This makes it possible to build a custom Socket-based IConnectionListenerFactory
without losing out on all the performance work and pooling happening in SocketConnection.
Look at this example of a custom IConnectionListenerFactory for an example of how to use this new API.
Streamlined HTTP/3 setup
RC1 introduces an easier setup experience for using HTTP/3 in Kestrel. All that’s needed is to configure Kestrel to use the proper protocol.
HTTP/3 can be enabled on all ports using ConfigureEndpointDefaults, or for an individual port, as in the sample below.
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel((context, options) =>
{
options.Listen(IPAddress.Any, 5001, listenOptions =>
{
// Use HTTP/3
listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
listenOptions.UseHttps();
});
});
HTTP/3 is not supported everywhere. See Use HTTP/3 with the ASP.NET Core Kestrel web server for information on getting started with HTTP/3 in Kestrel.
Upgrade to Duende Identity Server
Templates which use Identity Server have now be updated to use Duende Identity Server, as previously discussed in our announcement.
If you are extending the identity models you will need to update the namespaces in your code from IdentityServer4.IdentityServer
to Duende.IdentityServer
.
Please note the license model for Duende Identity Server has changed to a reciprocal license, which may require license fees if you use it commercially in production. You can check the Duende license page for more details.
Give feedback
We hope you enjoy this preview release of ASP.NET Core in .NET 6. We’re eager to hear about your experiences with this release. Let us know what you think by filing issues on GitHub.
Thanks for trying out ASP.NET Core!
The post ASP.NET Core updates in .NET 6 Release Candidate 1 appeared first on ASP.NET Blog.
Comments
Post a Comment