You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddMvcDisplayConventions(
new AppendAsterixToRequiredFieldLabels(),
new HtmlByNameConventionFilter(),
new LabelTextConventionFilter(),
new TextAreaByNameConventionFilter(),
new TextboxPlaceholderConventionFilter(),
new DisableConvertEmptyStringToNull())
.AddMvcValidationConventions()
.AddMvcDisplayAttributes()
.AddMvcInheritanceValidationAttributeAdapterProvider()
.AddMvcViewRenderer();
services.AddFluentMetadata();
Display Conventions
IDisplayConventionFilter classes transform the display metadata by convention.
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddMvcDisplayConventions(
new AppendAsterixToRequiredFieldLabels(),
new HtmlByNameConventionFilter(),
new LabelTextConventionFilter(),
new TextAreaByNameConventionFilter(),
new TextboxPlaceholderConventionFilter(),
new DisableConvertEmptyStringToNull());
Display Convention
Description
AppendAsterixToRequiredFieldLabels
Appends * to label automatically
HtmlByNameConventionFilter
If the field name contains 'html' DataTypeName will be set to 'Html'
LabelTextConventionFilter
If a display attribute name is not set on model property FirstName would become 'First Name'
TextAreaByNameConventionFilter
If the field name contains 'body' or 'comments' DataTypeName will be set to 'MultilineText'
TextboxPlaceholderConventionFilter
If Display attribute prompt is not set on model property FirstName would become 'First Name...'
DisableConvertEmptyStringToNull
By default MVC converts empty string to Null, this disables the convention
Validation Conventions
IValidationConventionFilter classes transform the validation metadata by convention.
public class SliderAttribute : Attribute, IDisplayMetadataAttribute
{
public int Min { get; set; } = 0;
public int Max { get; set; } = 100;
public SliderAttribute()
{
}
public SliderAttribute(int min, int max)
{
Min = min;
Max = max;
}
public void TransformMetadata(DisplayMetadataProviderContext context)
{
var propertyAttributes = context.Attributes;
var modelMetadata = context.DisplayMetadata;
var propertyName = context.Key.Name;
modelMetadata.DataTypeName = "Slider";
modelMetadata.AdditionalValues["Min"] = Min;
modelMetadata.AdditionalValues["Max"] = Max;
}
}
View Model
public class ViewModel
{
[Slider(0, 200)]
public int Value {get; set;}
}
Views\Shared\EditorTemplates\Slider.cshtml
@model Int32?
@Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue,
new { @class = "custom-range", @type = "range", min = (int)ViewData.ModelMetadata.AdditionalValues["Min"], max = (int)ViewData.ModelMetadata.AdditionalValues["Max"] })
Views\Shared\DisplayTemplates\Slider.cshtml
@model int?
@{
var value = Html.ViewData.TemplateInfo.FormattedModelValue;
var displayValue = "";
if (value != null)
{
displayValue = value.ToString();
}
}
@Html.Raw(displayValue)
Validation Attribute Inheritance
By default the ValidationAttributeAdapterProdiver doesn't perform client side validation if you inherit from an existing validation attribute.
The below service collection extensions overrides the existing IValidationAttributeAdapterProvider enabling client side validation for inherited types.
public class NumberValidatorAttribute : RangeAttribute
{
public NumberValidatorAttribute()
:base(0, int.MaxValue)
{
//The field {0} must be between {1} and {2}.
ErrorMessage = "The field {0} must be a number.";
}
}
Render Razor Views as Html
Gives the ability to render Views, Partials, Display for Model or Editor for Model.
Allows modelmetadata to be configured via fluent syntax rather than attributes.
services.AddFluentMetadata();
Example
public class PersonConfig : ModelMetadataConfiguration<Person>
{
public PersonConfig()
{
Configure(p => p.Name).Required();
Configure<string>("Name").Required();
}
}
Delimited Query Strings
[DelimitedQueryString(',', '|')]
public async Task<ActionResult<List<Model>>> BulkGetByIds(IEnumerable<string> ids)
{
}
Routing Attributes
Attribute
Description
AcceptHeaderMatchesMediaTypeAttribute
Action only executed if Accept Header matches
ContentTypeHeaderMatchesMediaTypeAttribute
Action only executed if Content-Type Header matches
RequestHeaderMatchesMediaTypeAttribute
Action only executed if header matches media type
AjaxRequestAttribute
Action only executed if X-Requested-With matches 'XMLHttpRequest'
NoAjaxRequestAttribute
Action only executed if X-Requested-With does not match 'XMLHttpRequest'
[HttpPost()]
[Consumes("application/json", "application/vnd.app.bookforcreation+json")]
[RequestHeaderMatchesMediaType(HeaderNames.ContentType,
"application/json", "application/vnd.app.bookforcreation+json")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity,
Type = typeof(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary))]
public async Task<ActionResult<Book>> CreateBook(
Guid authorId,
[FromBody] BookForCreation bookForCreation)
{
if (!await _authorRepository.AuthorExistsAsync(authorId))
{
return NotFound();
}
var bookToAdd = _mapper.Map<Entities.Book>(bookForCreation);
_bookRepository.AddBook(bookToAdd);
await _bookRepository.SaveChangesAsync();
return CreatedAtRoute(
"GetBook",
new { authorId, bookId = bookToAdd.Id },
_mapper.Map<Book>(bookToAdd));
}
[HttpPost()]
[Consumes("application/vnd.app.bookforcreationwithamountofpages+json")]
[RequestHeaderMatchesMediaType(HeaderNames.ContentType,
"application/vnd.app.bookforcreationwithamountofpages+json")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity,
Type = typeof(Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary))]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<ActionResult<Book>> CreateBookWithAmountOfPages(
Guid authorId,
[FromBody] BookForCreationWithAmountOfPages bookForCreationWithAmountOfPages)
{
if (!await _authorRepository.AuthorExistsAsync(authorId))
{
return NotFound();
}
var bookToAdd = _mapper.Map<Entities.Book>(bookForCreationWithAmountOfPages);
_bookRepository.AddBook(bookToAdd);
await _bookRepository.SaveChangesAsync();
return CreatedAtRoute(
"GetBook",
new { authorId, bookId = bookToAdd.Id },
_mapper.Map<Book>(bookToAdd));
}
Action Results
ActionResult
Description
CsvResult
Returns CSV from IEnumerable
HtmlResult
Returns text/html
JavaScriptResult
Returns application/javascript
Validation Attributes
Attribute
Description
AutoModelValidationAttribute
Checks ModelState.IsValid and returns view with with 400 status code if invalid.
ValidatableAttribute
if request contains x-action-intent:validate header only model validation occurs
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddMvcPointModelBinder()
.AddMvcRawStringRequestBodyInputFormatter()
.AddMvcRawBytesRequestBodyInputFormatter();
OR
services.AddMvc(options =>{
options.InputFormatters.Insert(0, new RawStringRequestBodyInputFormatter());
options.InputFormatters.Insert(0, new RawBytesRequestBodyInputFormatter());
});
Feature Folders
Business Component (Functional) organization over Categorical organization. It's very easy to stick with the standard structure but organizing into business components makes it alot easier to maintain and gives ability to easily copy/paste an entire piece of functionality.
Seach for Non Area views in the following locations /{RootFeatureFolder}/{Controller}/{View}.cshtml, /{RootFeatureFolder}/{Controller}/Views/{View}.cshtml and /{RootFeatureFolder}/Shared/Views/{View}.cshtml
Seach for Area views in the following locations /Areas/{Area}/{RootFeatureFolder}/{Controller}/{View}.cshtml, /Areas/{Area}/{RootFeatureFolder}/{Controller}/Views/{View}.cshtml, /Areas/{Area}/{RootFeatureFolder}/Shared/Views/{View}.cshtml, /Areas/{Area}/Shared/Views/{View}.cshtml and /{RootFeatureFolder}/Shared/Views/{View}.cshtml
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/auth")]
public class AuthController : ControllerBase
{
public AuthController()
{
}
}
Set App Culture when not using Request Localization
var cultureInfo = new CultureInfo("en-AU");
CultureInfo.DefaultThreadCurrentCulture = cultureInfo;
CultureInfo.DefaultThreadCurrentUICulture = cultureInfo;
Localization ASP.NET Core 2.2
Endpoint Routing (enabled by default from 2.2 onwards) changes how links are generated. Previously all ambient route data was passed to action links. Now ambient route data is only reused if generating link for the same controller/action. I have extended the UrlHelper implementation so route keys can be reqgistered as being globally ambient.
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public bool AlwaysIncludeCultureInUrl { get; set; } = true;
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddMvc(options =>
{
//https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-2.2
//https://github.com/aspnet/AspNetCore/blob/1c126ab773059d6a5899fc29547cb86ed49c46bf/src/Http/Routing/src/Template/TemplateBinder.cs
//EnableEndpointRouting = false Ambient route values always reused.
//EnableEndpointRouting = true. Ambient route values only reused if generating link for same controller/action.
options.EnableEndpointRouting = true;
if (AlwaysIncludeCultureInUrl)
options.AddCultureRouteConvention("cultureCheck");
else
options.AddOptionalCultureRouteConvention("cultureCheck");
//Middleware Pipeline - Wraps MVC
options.Filters.Add(new MiddlewareFilterAttribute(typeof(LocalizationPipeline)));
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization()
//If EnableEndpointRouting is enabled (enabled by default from 2.2) ambient route data is required.
.AddAmbientRouteDataUrlHelperFactory(options => {
options.AmbientRouteDataKeys.Add(new AmbientRouteData("area", false));
options.AmbientRouteDataKeys.Add(new AmbientRouteData("culture", true));
options.AmbientRouteDataKeys.Add(new AmbientRouteData("ui-culture", true));
});
services.AddCultureRouteConstraint("cultureCheck");
services.AddRequestLocalizationOptions(
defaultCulture: "en-AU",
supportAllCountryFormatting: false,
supportAllLanguagesFormatting: false,
supportUICultureFormatting: true,
allowDefaultCultureLanguage: true,
supportedUICultures: "en");
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IOptions<RequestLocalizationOptions> localizationOptions)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRequestLocalization(localizationOptions.Value);
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
});
var routeBuilder = new RouteBuilder(app);
if (AlwaysIncludeCultureInUrl)
{
routeBuilder.RedirectCulturelessToDefaultCulture(localizationOptions.Value);
}
app.UseRouter(routeBuilder.Build());
}
}
Localization ASP.NET Core 3.0
Endpoint Routing (enabled by default from 2.2 onwards) changes how links are generated. Previously all ambient route data was passed to action links. Now ambient route data is only reused if generating link for the same controller/action. I have extended the UrlHelper implementation so route keys can be reqgistered as being globally ambient.
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public bool AlwaysIncludeCultureInUrl { get; set; } = true;
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
});
services.AddControllersWithViews(options =>
{
options.EnableEndpointRouting = true;
if (AlwaysIncludeCultureInUrl)
options.AddCultureRouteConvention("cultureCheck");
else
options.AddOptionalCultureRouteConvention("cultureCheck");
//Middleware Pipeline - Wraps MVC
options.Filters.Add(new MiddlewareFilterAttribute(typeof(LocalizationPipeline)));
}).SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization()
//If EnableEndpointRouting is enabled (enabled by default from 2.2) ambient route data is required.
.AddAmbientRouteDataUrlHelperFactory(options =>
{
options.AmbientRouteDataKeys.Add(new AmbientRouteData("area", false));
options.AmbientRouteDataKeys.Add(new AmbientRouteData("culture", true));
options.AmbientRouteDataKeys.Add(new AmbientRouteData("ui-culture", true));
});
services.AddRazorPages();
services.AddCultureRouteConstraint("cultureCheck");
services.AddRequestLocalizationOptions(
defaultCulture: "en-AU",
supportAllCountryFormatting: false,
supportAllLanguagesFormatting: false,
supportUICultureFormatting: true,
allowDefaultCultureLanguage: true,
supportedUICultures: "en");
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IOptions<RequestLocalizationOptions> localizationOptions)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRequestLocalization(localizationOptions.Value);
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
if (AlwaysIncludeCultureInUrl)
{
endpoints.MapMiddlewareGet("{culture:cultureCheck}/{*path}", appBuilder =>
{
});
//redirect culture-less routes
endpoints.MapGet("{*path}", (RequestDelegate)(ctx =>
{
var defaultCulture = localizationOptions.Value.DefaultRequestCulture.Culture.Name;
var cultureFeature = ctx.Features.Get<IRequestCultureFeature>();
var actualCulture = cultureFeature?.RequestCulture.Culture.Name;
var actualCultureLanguage = cultureFeature?.RequestCulture.Culture.TwoLetterISOLanguageName;
var path = ctx.GetRouteValue("path") ?? string.Empty;
var culturedPath = $"{ctx.Request.PathBase}/{actualCulture}/{path}{ctx.Request.QueryString.ToString()}";
ctx.Response.Redirect(culturedPath);
return Task.CompletedTask;
}));
}
});
}
}
Db Initialization
public class Program
{
public static async Task Main (string[] args)
{
var webHost = CreateWebHostBuilder(args).Build();
using (var scope = webHost.Services.CreateScope())
{
var serviceProvider = scope.ServiceProvider;
var hostingEnvironment = serviceProvider.GetRequiredService<IHostingEnvironment>();
var appLifetime = serviceProvider.GetRequiredService<IApplicationLifetime>();
if (hostingEnvironment.IsDevelopment())
{
var ctx = serviceProvider.GetRequiredService<TennisBookingDbContext>();
await ctx.Database.MigrateAsync(appLifetime.ApplicationStopping);
try
{
var userManager = serviceProvider.GetRequiredService<UserManager<TennisBookingsUser>>();
var roleManager = serviceProvider.GetRequiredService<RoleManager<TennisBookingsRole>>();
await SeedData.SeedUsersAndRoles(userManager, roleManager);
}
catch (Exception ex)
{
var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("UserInitialisation");
logger.LogError(ex, "Failed to seed user data");
}
}
}
webHost.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureServices(services => services.AddAutofac())
.UseStartup<Startup>();
}
var timeZone = System.Net.WebUtility.UrlDecode(Request.Cookies["timezone"]);
Config + Logging
public class Program
{
public static IConfiguration Configuration;
public async static Task<int> Main(string[] args)
{
Configuration = Config.Build(args, Directory.GetCurrentDirectory(), typeof(TStartup).Assembly.GetName().Name);
LoggingInit.Init(Configuration);
try
{
Log.Information("Getting the motors running...");
var host = CreateWebHostBuilder(args).Build();
//https://andrewlock.net/running-async-tasks-on-app-startup-in-asp-net-core-part-2/
//Even though the tasks run after the IConfiguration and DI container configuration has completed, they run before the IStartupFilters have run and the middleware pipeline has been configured.
//await host.InitAsync();
//AppStartup.Configure will be called here
await host.RunAsync();
return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
return 1;
}
finally
{
Log.CloseAndFlush();
}
}
}