Generate Razor HTML emails in dotnet core 2 - razor

How can you generate emails (html) using Razor in dotnetcore - and not from an MVC app (think from a console app)?
RazorEngine does a great job in .net 4.x, but is not working in dotnet core.
RazorEngineLight works in dotnet core 1.x, but not in 2.x.
Some other options are mentioned in this post: Using Razor outside of MVC in .NET Core but none of them actually work in .net core 2.0
Edit two years later:
In case somebody comes here looking for answers on this... I (OP) have stopped entirely relying on Razor to generate emails using templates etc. It is very fragile and error-prone - a non-stop headache. I prefer Mandrill or Sendgrid these days - using templates.

In a comment on this provided answer from the link provided you stated
I am not able to get this to work. I get the error: Unable to resolve service for type 'Microsoft.AspNetCore.Mvc.Razor.IRazorViewEngine' while attempting to activate 'Mvc.RenderViewToString.RazorViewToStringRenderer'.'
This normally indicates that a required service was not registered with the service collection so the provider is unable to resolve the service when needed.
That answer did not refer to the additional service configuration and only had
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IViewRender, ViewRender>();
}
as it was already being run in an Asp.Net Core environment, which meant that the services manually added in the console application were already being done in start up.
Pay attention to this snippet from the answer that was linked to from the answer you commented on.
private static void ConfigureDefaultServices(IServiceCollection services) {
var applicationEnvironment = PlatformServices.Default.Application;
services.AddSingleton(applicationEnvironment);
var appDirectory = Directory.GetCurrentDirectory();
var environment = new HostingEnvironment
{
WebRootFileProvider = new PhysicalFileProvider(appDirectory),
ApplicationName = "RenderRazorToString"
};
services.AddSingleton<IHostingEnvironment>(environment);
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Clear();
options.FileProviders.Add(new PhysicalFileProvider(appDirectory));
});
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
services.AddSingleton<DiagnosticSource>(diagnosticSource);
services.AddLogging();
services.AddMvc();
services.AddSingleton<RazorViewToStringRenderer>();
}
The important part above is
services.AddMvc();
That will add the relevant view engine dependencies to the service collection
MvcServiceCollectionExtensions.cs
public static IMvcBuilder AddMvc(this IServiceCollection services) {
//...code removed for brevity
// Default framework order
builder.AddFormatterMappings();
builder.AddViews();
builder.AddRazorViewEngine();
builder.AddRazorPages();
builder.AddCacheTagHelper();
//...code removed for brevity
}
Everything else as currently presented is sound and should work as intended.
You should review
https://github.com/aspnet/Entropy/tree/93ee2cf54eb700c4bf8ad3251f627c8f1a07fb17/samples/Mvc.RenderViewToString
and follow a similar structure to get the code to work in your scenario. From there you can start making your custom modification and monitor where it breaks.
The modular nature of .Net Core allows for such customizations as the different modules can be stripped out and used in other environments.

Related

ASP.NET Core 6 MVC + views: exception when switching from AddDefaultIdentity to AddIdentity

I have opened a test project (.NET 6, VS2022) based on ASP.NET Core MVC and views template (not Razor pages), with activated individual user accounts.
Program.cs looks like this (from the template):
builder.Services.AddDefaultIdentity<IdentityUser>(options ...
builder.Services.AddControllersWithViews();
...
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
So far so good.
Now I added some example code to seed the user database, which needs access to the RoleManager:
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();`
However, that throws an exception
No service for type Microsoft.AspNetCore.Identity.RoleManager`1[Microsoft.AspNetCore.Identity.IdentityRole]'
which could be fixed (thanks Stackoverflow) by changing the AddDefaultIdentity() to AddIdentity() which introduces IdentityRole:
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options ...
However, now I get an exception further down with
app.MapRazorPages();
System.InvalidOperationException: 'Unable to find the required services. Please add all the required services by calling 'IServiceCollection.AddRazorPages' inside the call to 'ConfigureServices(...)' in the application startup code.'
Which services need to be configured and how?
When I remove app.MapRazorPages();, the user management pages (login, user registration) do no longer work (404 error).
When I instead add builder.Service.AddRazorPages() above, the routing is also broken: a route to "/account/login" is missing, probably because Razor pages are somewhat differently organized than MVC controllers. Obviously, I do not want Razor pages, just Razor logic in a few views, and basically MVC architecture.
I am, honestly, a bit confused, since the official documentation does not help much.
Follow your document, I fount that what you did is adding an initialization to create data in the database. So I created a new .net 6 MVC app and integrate default authentication. Pick up Authentication type field with Individual User Accounts when creating the project, then run Update-Database command in Package Manager Console window. Now I have a empty .net 6 MVC project with default asp.net core authentication.
Next, I followed the document and created a SeedData.cs file in the root folder:
using Microsoft.AspNetCore.Identity;
namespace WebAppDefIdentity
{
public static class SeedData
{
//public const string AdministratorRole = "Administrator";
public static async Task InitializeAsync(IServiceProvider services) {
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
await EnsureRolesAsync(roleManager);
}
private static async Task EnsureRolesAsync(RoleManager<IdentityRole> roleManager)
{
var alreadyExists = await roleManager.RoleExistsAsync("Administrator");
}
}
}
And the document is .net 5 oriented project, so need a little change. In the Program.cs file, adding following codes.
using (var scope = app.Services.CreateScope())
{
var aa = scope.ServiceProvider;
await SeedData.InitializeAsync(aa);
}
Then I reproduce your first exception
To solve this exception, I changed in Program.cs with code .AddRoles<IdentityRole>()
Then no exception.

How to use localization in Razor Class Library in Asp.Net Core

I have tried to create the Razor Class Library with Asp.Net Core in following project structure:
I have used in my web application these settings for localization in Startup class:
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddViewLocalization(
LanguageViewLocationExpanderFormat.Suffix,
opts => { opts.ResourcesPath = "Resources"; })
.AddDataAnnotationsLocalization();
services.Configure<RequestLocalizationOptions>(
opts =>
{
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("en")
};
opts.DefaultRequestCulture = new RequestCulture("en");
opts.SupportedCultures = supportedCultures;
opts.SupportedUICultures = supportedCultures;
});
....
var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(options.Value);
In Index.cshtml:
#using Microsoft.AspNetCore.Mvc.Localization
#inject IViewLocalizer Localizer
<h1>#Localizer["Title"]</h1>
Unfortunately, the result is only string "Title". I can't load these resx files from Razor Class Library.
How can I use the localization in Razor Class Library like above?
UPDATE: This is very similiar use case - https://github.com/aspnet/Localization/issues/328 - that provides some example.
I haven't tried the accepted answer and based on the comments, it seems the OP didn't get it to work. I implemented a pattern similar to the View/Page locator pattern that MVC/Razor Pages uses namely, that resources can be provided in a RCL or separate assembly and use ViewLocalizer and it'll just find the matching resource string from the highest precedence resource. You can read my implementation and see if it might work for you.
https://terryaney.wordpress.com/2021/01/04/migrating-to-net-core-overridable-localization-in-razor-class-libraries/
You appear to have forgotten to configure localization correctly using AddLocalization
Using details provided from documentation
Reference Globalization and localization in ASP.NET Core
Configure localization
Localization is configured in the ConfigureServices method:
services.AddLocalization(options => options.ResourcesPath = "Resources"); //<<< This is required
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
.AddDataAnnotationsLocalization();
AddLocalization Adds the localization services to the services container. The code above also sets the resources path to "Resources".
AddViewLocalization Adds support for localized view files.
AddDataAnnotationsLocalization Adds support for localized DataAnnotations validation messages through IStringLocalizer abstractions.
Localization middleware
The current culture on a request is set in the localization Middleware. The localization middleware is enabled in the Configure method. The localization middleware must be configured before any middleware which might check the request culture (for example, app.UseMvcWithDefaultRoute()).
var supportedCultures = new[] {
new CultureInfo("en-US"),
new CultureInfo("en")
};
app.UseRequestLocalization(new RequestLocalizationOptions{
DefaultRequestCulture = new RequestCulture("en"),
// Formatting numbers, dates, etc.
SupportedCultures = supportedCultures,
// UI strings that we have localized.
SupportedUICultures = supportedCultures;
});
//...other middleware
app.UseMvcWithDefaultRoute();
The path to the resource file shown in the example image follows the path naming convention given that
you are using the ResourcesPath option which was set to "Resources". This should allow the view to find the resource file in the relative path to the "Resources" folder.
An alternative is to not use the ResourcesPath option, and place the .resx file in the same folder as the view, following the naming convention of course.
Base on additional details provided it was indicated that the UI project would be packaged as a nuget package.
Then have the resources files packaged into the nuget package and have them unpacked to the resources folder of the target project when when installed.
The resources need to be in the site root to be available to the view, so you then need to reference all the files in your .nuspec:
<?xml version="1.0"?>
<package>
<metadata>...
</metadata>
<files>
<!-- Add all resource files -->
<file src="Resources\**\*.resx" target="content\Resources" />
</files>
</package>
Reference Creating NuGet packages

.net core, n-layered app, should services layer have dependency on Microsoft.Extensions.Options.dll

Straightforward question is: are Microsoft.Extensions.Options.IOptions meant to be used only within the context of umbrella app (web app in this case) or in class libraries also?
Example:
In a n-layered, asp.net core app we have services layer that is dependant on some settings coming from appsettings.json file.
What we first started with is something along these lines in Startup.cs:
services.Configure<Services.Options.XOptions>(options =>
{
options.OptionProperty1 = Configuration["OptionXSection:OptionXProperty"];
});
And then in service constructor:
ServiceConstructor(IOptions<XOptions> xOptions){}
But that assumes that in our Service layer we have dependecy on Microsoft.Extensions.Options.
We're not sure if this is recomended way or is there some better practice?
It just feels a bit awkward our services class library should be aware of DI container implementation.
You can register POCO settings for injection too, but you lose some functionalities related to when the appsettings.json gets edited.
services.AddTransient<XOptions>(
provider => provider.GetRequiredService<IOptionsSnapshot<XOptions>>().Value);
Now when you inject XOptions in constructor, you will get the class. But when your edit your appsettings.json, the value won't be updated until the next time it's resolved which for scoped services would be on next request and singleton services never.
On other side injecting IOptionsSnapshot<T> .Value will always get you the current settings, even when appsettings.json is reloaded (assuming you registered it with .AddJsonFile("appsettings.json", reloadOnSave: true)).
The obvious reason to keep the functionality w/o pulling Microsoft.Extensions.Options package into your service/domain layer will be create your own interface and implementation.
// in your shared service/domain assembly
public interface ISettingsSnapshot<T> where T : class
{
T Value { get; }
}
and implement it on the application side (outside of your services/domain assemblies), i.e. MyProject.Web (where ASP.NET Core and the composition root is)
public class OptionsSnapshotWrapper<T> : ISettingsSnapshot<T>
{
private readonly IOptionsSnapshot<T> snapshot;
public OptionsSnapshotWrapper(IOptionsSnapshot<T> snapshot)
{
this.snapshot = snapshot ?? throw new ArgumentNullException(nameof(snapshot));
}
public T Value => snapshot.Value;
}
and register it as
services.AddSingleton(typeof(ISettingsSnapshot<>), typeof(OptionsSnapshotWrapper<T>));
Now you have removed your dependency on IOptions<T> and IOptionsSnapshot<T> from your services but retain all up advantages of it like updating options when appsettings.json is edited. When you change DI, just replace OptionsSnapshotWrapper<T> with your new implementation.

ASP.NET 5: Configuring IdentityServer3 authentication

I've just started digging into the new ASP.NET 5 by creating a test single page application with the OAuth login. I already know that I can use IdentityServer3 for that purpose and it seems pretty nice. I've found a post by Dominick Baier which is explaining how to set up the IdentityServer3. However, the post seems to be out of date or the identity server itself isn't working with the latest version of the ASP.NET 5 (which is beta7 at the moment).
The problem is, when I try to configure the IdentityServer in the Startup.cs I got an error from VS telling me that IApplicationBuilder has no extension method called UseIdentityServer. And this seems to be true, since in the IdentityServer3 source code they have this extension method declared for IAppBuilder (not IApplicationBuilder).
Here is my code (Startup.cs):
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Add MVC to the request pipeline.
app.UseMvc();
var options = new IdentityServerOptions
{
Factory = new IdentityServerServiceFactory()
};
app.UseIdentityServer(options);
}
And the error (on the last line) is
'IApplicationBuilder' does not contain a definition for 'UseIdentityServer' and the best extension method overload 'UseIdentityServerExtension.UseIdentityServer(IAppBuilder, IdentityServerOptions)' requires a receiver of type 'IAppBuilder'
Obviously, if I change the parameter type in the Configure method to IAppBuiler, it'll throw a runtime error because the dependency injection will not be able to inject that type. Even if it would, I'd lose the UseMvc() extension method.
So could you point me in the right direction please?
Perhaps I'm just missing something tiny but crucial here.
Thanks in advance!

Using the Box Windows (.NET) V2 API in Web C# project

Is it possible to use the following SDK for a Web Application:
https://github.com/box/box-windows-sdk-v2
The specs say it is targeted for the .NET framework for Windows and Windows Phone applications, but I wasn't able to figure it out for a Web .NET project. Is this SDK strictly for native Windows and Windows phones applications?
I've never used await/async functions in C# and that's possibly where I can't figure it out. I've been able to successfully get an oauth2 token/refresh token, but don't know where to go from here. Could anyone take a look or provide a sample of how to retrieve folder details?
Thanks !
It is absolutely possible to use the Box Windows SDK in an ASP.NET web application.
I've recently updated the readme documentation to mention that the SDK supports the .NET 4.0 framework, so as long as your project is targeting that framework or above you should be good to go. If you are running an express version of Visual Studio, you unfortunately will not be able to open the SDK source project as it's built as a PCL (Portable Class Library). You can, however, still get the binaries through nuget.
One important thing you have to remember when using async/await calls in ASP.NET is that you have to include the Async="true" attribute in your Page declaration:
<%# Page Language="C#" Async="true" %>
You mentioned that you were able to successfully get OAuth2 tokens/refresh tokens. I'm not sure if you wrote your own workflow to retrieve those tokens, but the SDK supports handling the second step of exchanging an auth code for tokens as follows:
public async Task Authenticate(string authCode)
{
BoxConfig config = new BoxConfig("YOUR_CLIENT_KEY", "YOUR_CLIENT_SECRET", new Uri("YOUR_REDIRECT"));
BoxClient client = new BoxClient(config);
await client.Auth.AuthenticateAsync("authCode");
}
Note that when using async/await, you must decorate your method signature with the "async" keyword. Microsoft has written great articles on further understanding the async/await keywords.
In the case that you built your own OAuth2 workflow, the SDK also accepts a completed OAuth Session. Here's a full sample of that, and an example of getting items in your root level folder:
public partial class WebForm1 : System.Web.UI.Page
{
BoxClient _client;
protected async void Page_Load(object sender, EventArgs e)
{
BoxConfig config = new BoxConfig("YOUR_CLIENT_KEY", "YOUR_CLIENT_SECRET", new Uri("https://YOUR_REDIRECT"));
BoxClient client = new BoxClient(config);
OAuthSession session = new OAuthSession("YOUR_ACCESS_TOKEN", "YOUR_REFRESH_TOKEN", 3600, "bearer");
_client = new BoxClient(config, session);
}
protected async void Button_Click(object sender, EventArgs e)
{
BoxFolder folder = await _client.FoldersManager.GetItemsAsync("0", 10);
string test = folder.Name;
}
}