I've read the Microsoft documentation of fundamentals for Options and Configuration, but still can't find the right way to extract configuration into an object while validating data annotations.
One approach I tried in Startup.ConfigureServices
services.AddOptions<EmailConfig>().Bind(Configuration.GetSection("Email")).ValidateDataAnnotations();
This "should" allow accessing the configuration by adding this in the class constructor: (IOptions<EmailConfig> emailConfig)
However it's not working.
Another approach is to add (IConfiguration configuration) to the constructor, but this doesn't allow me to call ValidateDataAnnotations.
configuration.GetSection("Email").Get<EmailConfig>();
First question: does the responsibility to bind and validate the configuration belong to the Startup class or to the class using it? If it's used by several classes I'd say it belongs to Startup; and the class could be used in another project with different configuration layout.
Second question: what is the correct syntax to bind and validate the configuration so it can be accessed from the class?
Third question: if I'm validating through data annotations in Startup, then the class using the configuration simply assumes the configuration is valid and I don't put any re-validation whatsoever?
UPDATE: After gaining more experience and reviewing the structure of all my code, I changed my approach to follow standard patterns.
The following code DOES work... but only validates it when used. This can be registered in a class library and won't throw any errors until the particular service is used.
services.AddOptions<EmailConfig>()
.Bind(configuration.GetSection("Email"))
.ValidateDataAnnotations();
Then, in Configure, I add this to force validation of needed configuration values at startup (CheckNotNull is a custom extension method, what matters is simply that you call IOptions.Value
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app?.ApplicationServices.GetService<IOptions<EmailConfig>>().Value.CheckNotNull("Config: Email");
app?.ApplicationServices.GetService<IOptions<OntraportConfig>>().Value.CheckNotNull("Config: Ontraport");
...
Then in the class using it
public class EmailService(IOptions<EmailConfig> config)
You can try validating the class yourself in start up before adding it to service collection.
Startup
var settings = Configuration.GetSection("Email").Get<EmailConfig>();
//validate
var validationResults = new List<ValidationResult>();
var validationContext = new ValidationContext(settings, serviceProvider: null, items: null);
if (!Validator.TryValidateObject(settings, validationContext, validationResults,
validateAllProperties: true)) {
//...Fail early
//will have the validation results in the list
}
services.AddSingleton(settings);
That way you are not coupled to IOptions and you also allow your code to fail early and you can explicitly inject the dependency where needed.
You could package the validation up into your own extension method like
public static T GetValid<T>(this IConfiguration configuration) {
var obj = configuration.Get<T>();
//validate
Validator.ValidateObject(obj, new ValidationContext(obj), true);
return obj;
}
for calls like
EmailConfig emailSection = Configuration.GetSection("Email").GetValid<EmailConfig>();
services.AddSingleton(emailSection);
Internally, ValidateDataAnnotations is basically doing the same thing.
/// <summary>
/// Validates a specific named options instance (or all when name is null).
/// </summary>
/// <param name="name">The name of the options instance being validated.</param>
/// <param name="options">The options instance.</param>
/// <returns>The <see cref="ValidateOptionsResult"/> result.</returns>
public ValidateOptionsResult Validate(string name, TOptions options)
{
// Null name is used to configure all named options.
if (Name == null || name == Name)
{
var validationResults = new List<ValidationResult>();
if (Validator.TryValidateObject(options,
new ValidationContext(options, serviceProvider: null, items: null),
validationResults,
validateAllProperties: true))
{
return ValidateOptionsResult.Success;
}
return ValidateOptionsResult.Fail(String.Join(Environment.NewLine,
validationResults.Select(r => "DataAnnotation validation failed for members " +
String.Join(", ", r.MemberNames) +
" with the error '" + r.ErrorMessage + "'.")));
}
// Ignored if not validating this instance.
return ValidateOptionsResult.Skip;
}
Source Code
Update from the Future
Newer versions of .NET added more extension methods to simplify this.
Note: Technically these are all from Microsoft.Extensions.XYZ packages released alongside .NET. It's possible that these packages are compatible with earlier versions of .NET as well, but I haven't verified backward-compatibility.
OP's Example
services.AddOptions<EmailConfig>()
.Bind(configuration.GetSection("Email"))
.ValidateDataAnnotations();
Can now be simplified to:
// Requires .NET 5 extensions or greater
services.AddOptions<EmailConfig>()
.BindConfiguration("Email")
.ValidateDataAnnotations();
...and for eager validation at startup (rather than when options are used), we can add a single line:
// Requires .NET 6 extensions or greater
services.AddOptions<EmailConfig>()
.BindConfiguration("Email")
.ValidateDataAnnotations()
.ValidateOnStart();
Source/Credit
I learned about these updates from Andrew Lock's blog post. Credit and thanks go to him: Adding validation to strongly typed configuration objects in .NET 6
There is still no answer as to how ValidateDataAnnotations work, but based on Nkosi's answer, I wrote this class extension to easily run the validation on-demand. Because it's an extension on Object, I put it into a sub-namespace to only enable it when needed.
namespace Websites.Business.Validation {
/// <summary>
/// Provides methods to validate objects based on DataAnnotations.
/// </summary>
public static class ValidationExtensions {
/// <summary>
/// Validates an object based on its DataAnnotations and throws an exception if the object is not valid.
/// </summary>
/// <param name="obj">The object to validate.</param>
public static T ValidateAndThrow<T>(this T obj) {
Validator.ValidateObject(obj, new ValidationContext(obj), true);
return obj;
}
/// <summary>
/// Validates an object based on its DataAnnotations and returns a list of validation errors.
/// </summary>
/// <param name="obj">The object to validate.</param>
/// <returns>A list of validation errors.</returns>
public static ICollection<ValidationResult> Validate<T>(this T obj) {
var Results = new List<ValidationResult>();
var Context = new ValidationContext(obj);
if (!Validator.TryValidateObject(obj, Context, Results, true))
return Results;
return null;
}
}
}
Then in Startup it's quite straightforward
EmailConfig EmailSection = Configuration.GetSection("Email").Get<EmailConfig>().ValidateAndThrow();
services.AddSingleton<EmailConfig>(EmailSection);
Works like a charm; actually works like I'd expect ValidateDataAnnotations to work.
You can also use a method to validate all IOptions in your IOC conainter
private void CheckConfiguration(IApplicationBuilder app, IServiceCollection services)
{
var optionsServiceDescriptors = services.Where(s => s.ServiceType.Name.Contains("IOptionsChangeTokenSource"));
foreach (var service in optionsServiceDescriptors)
{
var genericTypes = service.ServiceType.GenericTypeArguments;
if (genericTypes.Length > 0)
{
var optionsType = genericTypes[0];
var genericOptions = typeof(IOptions<>).MakeGenericType(optionsType);
dynamic instance = app.ApplicationServices.GetService(genericOptions);
var options = instance.Value;
var results = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(options, new ValidationContext(options), results, true);
if (!isValid)
{
var messages = new List<string> { "Configuration issues" };
messages.AddRange(results.Select(r => r.ErrorMessage));
throw new Exception(string.Join("\n", messages));
}
}
}
}
You can find a example here : https://github.com/michelcedric/GetRequiredSectionSample/blob/feature/add-check-configuration/GetRequiredSectionSample/Startup.cs#L73
Related
I am doing a migration and have encountered an exception. Autofac can resolve my service from an instance of a service provider, but cannot get it in my controller at the time of the request.
Exception: Castle.DynamicProxy.ProxyGe
nerationException: This is a DynamicProxy2 error: Target type for the proxy implements Castle.DynamicProxy.IP
roxyTargetAccessor which is a DynamicProxy infrastructure interface and you should never implement it yoursel
f. Are you trying to proxy an existing proxy?
My AutoregistrableModule looks like that:
public class AutoregisterableModule : Autofac.Module
{
private readonly string _nameFilter;
/// <summary>
/// Default ctor
/// </summary>
/// <param name="nameFilter">Load will only search assemblies with names that contains filter</param>
public AutoregisterableModule(string nameFilter)
{
_nameFilter = nameFilter;
}
/// <summary>
/// Register's dependencies
/// </summary>
protected override void Load(ContainerBuilder builder)
{
var assemblies = new List<Assembly>();
var dependencies = DependencyContext.Default.RuntimeLibraries.Where(x => x.Name.Contains(_nameFilter));
foreach (var library in dependencies)
{
var assembly = Assembly.Load(new AssemblyName(library.Name));
assemblies.Add(assembly);
}
builder.Register(c => new LogInterceptor()).InstancePerLifetimeScope();
var isProd = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production";
var containerBuilder = builder.RegisterAssemblyTypes(assemblies.ToArray())
.Where(TypesToRegisterFilter)
.PreserveExistingDefaults()
.AsImplementedInterfaces();
if (!isProd)
{
containerBuilder.EnableInterfaceInterceptors();
}
base.Load(builder);
}
private static bool TypesToRegisterFilter(Type type)
{
return !type.IsSubclassOf(typeof(SwaggerConfigureOptions)) && !typeof(BackgroundService).IsAssignableFrom(type);
}
}
If I remove that row all works well(except of my LoggerInterceptor :D)
containerBuilder.EnableInterfaceInterceptors();
My service looks like
[Intercept(typeof(LogInterceptor))]
public class CategoriesService : ICategoriesService
{
// some code here
}
I tried to fix it by downgranding Autofac.Extensions.DependencyInjection package from 6.0.0 to 5.0.1 as were suggested there but it didn't help.
You can find full Stacktrace there https://pastebin.com/YCAhLQv0 .
So the question is how can I fix the exception and keep my interceptor working?
Fixed by raising Autofac.Extras.DynamicProxy package version from 4.5.0 to 5.0.0.
Hope it may help someone.
I have been reading the official Session and application state documentation and have stumbled upon the following paragraph:
Loading Session asynchronously
The default session provider in ASP.NET Core loads the session record
from the underlying IDistributedCache store asynchronously only if the
ISession.LoadAsync method is explicitly called before the TryGetValue,
Set, or Remove methods. If LoadAsync is not called first, the
underlying session record is loaded synchronously, which could
potentially impact the ability of the app to scale.
To have applications enforce this pattern, wrap the
DistributedSessionStore and DistributedSession implementations with
versions that throw an exception if the LoadAsync method is not called
before TryGetValue, Set, or Remove. Register the wrapped versions in
the services container.
The wrapping itself is not an issue for me, but in order to implement it, I need:
Reference to the original implementation
Registering the wrapped version
Currently, I have created the following wrapper class:
public class WrappedDistributedSession : ISession
{
private DistributedSession _service;
private bool loaded = false;
public WrappedDistributedSession(DistributedSession service)
{
_service = service;
}
public bool IsAvailable => _service.IsAvailable;
public string Id => _service.Id;
public IEnumerable<string> Keys => _service.Keys;
public void Clear() => _service.Clear();
public Task CommitAsync() => _service.CommitAsync();
public Task LoadAsync()
{
loaded = true;
return _service.LoadAsync();
}
public void Remove(string key)
{
if(loaded)
{
_service.Remove(key);
} else
{
throw new Exception();
}
}
public void Set(string key, byte[] value)
{
if (loaded)
{
_service.Set(key, value);
}
else
{
throw new Exception();
}
}
public bool TryGetValue(string key, out byte[] value)
{
if (loaded)
{
return _service.TryGetValue(key, out value);
}
else
{
throw new Exception();
}
}
}
And I have registered it in the Startup.ConfigureServices
services.AddScoped<ISession, WrappedDistributedSession>();
Obviously, since I am writing this question, my solution does not work. Where did I go wrong and how does one "Register the wrapped versions in the services container"?
Use at your risk. This seems to work in Configure method just after sessions.
This solution is an adaptation from this unit test:
https://github.com/dotnet/aspnetcore/blob/cd0eab88eaa230fa276c27ab5dc71ea267efe14f/src/Middleware/Session/test/SessionTests.cs#L654-L656
app.UseSession();
app.Use(async (context, next) =>
{
await context.Session.LoadAsync();
await next();
});
Or as a more qualified wrapper extension:
public static class SesssionAsyncExtensions
{
/// <summary>
/// Have sessions be asyncronous. This adaptation is needed to force the session provider to use async calls instead of syncronous ones for session.
/// Someone surprisingly for something that seems common, Microsoft didn't make this aspect super nice.
/// </summary>
/// <param name="app">App builder instance.</param>
/// <returns>App builder instance for chaining.</returns>
/// <remarks>
/// From Microsoft Documentation (https://learn.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-5.0):
/// The default session provider in ASP.NET Core will only load the session record from the underlying IDistributedCache store asynchronously if the
/// ISession.LoadAsync method is explicitly called before calling the TryGetValue, Set or Remove methods.
/// Failure to call LoadAsync first will result in the underlying session record being loaded synchronously,
/// which could potentially impact the ability of an application to scale.
///
/// See also:
/// https://github.com/dotnet/aspnetcore/blob/d2a0cbc093e1e7bb3e38b55cd6043e4e2a0a2e9a/src/Middleware/Session/src/DistributedSession.cs#L268
/// https://github.com/dotnet/AspNetCore.Docs/issues/1840#issuecomment-454182594
/// https://bartwullems.blogspot.com/2019/12/aspnet-core-load-session-state.html
/// </remarks>
public static IApplicationBuilder UseAsyncSession(this IApplicationBuilder app)
{
app.UseSession();
app.Use(async (context, next) =>
{
await context.Session.LoadAsync();
await next();
});
return app;
}
}
It seems you need to implement ISessonStore too (which is actually mentioned in the documentation you quoted), as it's the only one registered in AddSession extension method.
public static IServiceCollection AddSession(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddTransient<ISessionStore, DistributedSessionStore>();
services.AddDataProtection();
return services;
}
ISessionStore (and hence DistributedSessionStore) has a Create (see source) method which returns ISession. Here you need to return your custom implementation.
https://github.com/aspnet/Session/blob/rel/1.1.0/src/Microsoft.AspNetCore.Session/SessionServiceCollectionExtensions.cs#L27-L29
Then you can add before AddSession with
services.AddTransient<ISessionStore, AsyncDistributedSessionStore>();
I'm actually migrating some parts of my previous WCF services to Web API.
I had used QueryInterceptor on my Machine entity which checks whether the current user has access to the desired data and returns all the data or a filtered set that they are allowed to see.
[QueryInterceptor("Machines")]
public Expression<Func<Machine, bool>> FilterMachines()
{
return CheckMachineAccess<Machine>(m => m.MachineRole==xyz && m.userHasPermission);
}
I'm finding it difficult to implement the same in Web API. I'm using odata v4, OWIN hosted web API.
Anyone has any suggestions regarding this? Thanks in advance :)
Edit:
I have followed this approach. Don't know if this is the right way to follow.
[HttpGet]
[ODataRoute("Machines")]
[EnableQuery]
public IQueryable<Machine> FilterMachines(ODataQueryOptions opts)
{
var expression = CheckMachineAccess<Machine>(m => m.MachineRole==xyz && m.userHasPermission);
var result = db.Machines.Where(expression);
return (IQueryable<Machine>)result;
}
OP you are on the right track, if that is working for you then I totally support it!
I'll address the Title of your question directly first.
While using middleware is a good way to intercept incoming requests for Authentication and Access control, it is not a great way to implement row level security or to manipulate the query used in your controller.
Why? To manipulate the query for the controller, before the request is passed to the controller your middleware code will need to know so much about the controller and the data context that a lot of code will be duplicated.
In OData services, a good replacement for the many QueryInterceptor implementations is to Inherit from the EnableQuery Attribute.
[AttributeUsage(validOn: AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class EnableQueryAttribute : System.Web.OData.EnableQueryAttribute
{
public EnableQueryAttribute()
{
// TODO: Reset default values
}
/// <summary>
/// Intercept before the query, here we can safely manipulate the URL before the WebAPI request has been processed so before the OData context has been resolved.
/// </summary>
/// <remarks>Simple implementation of common url replacement tasks in OData</remarks>
/// <param name="actionContext"></param>
public override void OnActionExecuting(HttpActionContext actionContext)
{
var tokens = HttpUtility.ParseQueryString(actionContext.Request.RequestUri.AbsoluteUri);
// If the caller requested oDataV2 $inlinecount then remove it!
if (tokens.AllKeys.Contains("$inlinecount"))
{
// CS: we don't care what value they requested, OData v4 will only return the allPages count
tokens["$count"] = "true";
tokens.Remove("$inlinecount");
}
// if caller forgot to ask for count and we are top'ing but paging hasn't been configured lets add the overall count for good measure
else if (String.IsNullOrEmpty(tokens["$count"])
&& !String.IsNullOrEmpty(tokens["$top"])
&& this.PageSize <= 0
)
{
// we want to add $count if it is not there
tokens["$count"] = "true";
}
var modifiedUrl = ParseUri(tokens);
// if we modified the url, reset it. Leaving this in a logic block to make an obvious point to extend the process, say to perform other clean up when we know we have modified the url
if (modifiedUrl != actionContext.Request.RequestUri.AbsoluteUri)
actionContext.Request.RequestUri = new Uri(modifiedUrl);
base.OnActionExecuting(actionContext);
}
/// <summary>
/// Simple validator that can fix common issues when converting NameValueCollection back to Uri when the collection has been modified.
/// </summary>
/// <param name="tokens"></param>
/// <returns></returns>
private static string ParseUri(System.Collections.Specialized.NameValueCollection tokens)
{
var query = tokens.ToHttpQuery().TrimStart('=');
if (!query.Contains('?')) query = query.Insert(query.IndexOf('&'), "?");
return query.Replace("?&", "?");
}
/// <summary>
/// Here we can intercept the IQueryable result AFTER the controller has processed the request and created the intial query.
/// </summary>
/// <remarks>
/// So you could append filter conditions to the query, but, like middleware you may need to know a lot about the controller
/// or you have to make a lot of assumptions to make effective use of this override. Stick to operations that modify the queryOptions
/// or that conditionally modify the properties on this EnableQuery attribute
/// </remarks>
/// <param name="queryable">The original queryable instance from the controller</param>
/// <param name="queryOptions">The System.Web.OData.Query.ODataQueryOptions instance constructed based on the incomming request</param>
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
// I do not offer common examples of this override, because they would be specific to your business logic, but know that it is an available option
return base.ApplyQuery(queryable, queryOptions);
}
}
But how do we solve your issue of what is effectively an implementation of Row Level Security?
What you have implemented already is very similar to what I would have done. You are right, in your controller method you have enough information about the
context to be able to apply a filter to your query.
I had a similar idea in my projects and have a common base class for all my controllers that has a single method that all inheriting controllers must use to get the initial filtered query for their respective entity type:
The following are a cut down version of my base class methods for applying security style rules to a query
/// <summary>
/// Get the base table query for this entity, with user policy applied
/// </summary>
/// <returns>Default IQueryable reference to use in this controller</returns>
protected Task<IQueryable<TEntity>> GetQuery()
{
var dbQuery = this.GetEntityQuery();
return this.ApplyUserPolicy(dbQuery);
}
/// <summary>
/// Inheriting classes MUST override this method to include standard related tables to the DB query
/// </summary>
/// <returns></returns>
protected abstract DbQuery<TEntity> GetEntityQuery();
/// <summary>
/// Apply default user policy to the DBQuery that will be used by actions on this controller.
/// </summary>
/// <remarks>
/// Allow inheriting classes to implement or override the DBQuery before it is parsed to an IQueryable, note that you cannot easily add include statements once it is IQueryable
/// </remarks>
/// <param name="dataTable">DbQuery to parse</param>
/// <param name="tokenParameters">Security and Context Token variables that you can apply if you want to</param>
/// <returns></returns>
protected virtual IQueryable<TEntity> ApplyUserPolicy(DbQuery<TEntity> dataTable, System.Collections.Specialized.NameValueCollection tokenParameters)
{
// TODO: Implement default user policy filtering - like filter by tenant or customer.
return dataTable;
}
So now in your controller you would override the ApplyUserPolicy method to evaluate your security rules in the specific context of the Machine data, which would result in the following changes to your endpoint.
Note that I have also included additional endpoints to show how with this pattern ALL endpoints in your controller
should use GetQuery() to ensure they have the correct security rules applied.
The implication of this pattern though is that A single item Get will return not found instead of access denied if the item is not
found because it is out of scope for that user. I prefer this limitation because my user should not have any knowledge that the other data
that they are not allowed to access exists.
/// <summary>
/// Check that User has permission to view the rows and the required role level
/// </summary>
/// <remarks>This applies to all queries on this controller</remarks>
/// <param name="dataTable">Base DbQuery to parse</param>
/// <returns></returns>
protected override IQueryable<Machine> ApplyUserPolicy(DbQuery<Machine> dataTable)
{
// Apply base level policies, we only want to add further filtering conditions, we are not trying to circumvent base level security
var query = base.ApplyUserPolicy(dataTable, tokenParameters);
// I am faking your CheckMachineAccess code, as I don't know what your logic is
var role = GetUserRole();
query = query.Where(m => m.MachineRole == role);
// additional rule... prehaps user is associated to a specific plant or site and con only access machines at that plant
var plant = GetUserPlant();
if (plant != null) // Maybe plant is optional, so admin users might not return a plant, as they can access all machines
{
query = query.Where(m => m.PlantId == plant.PlantId);
}
return query;
}
[HttpGet]
[ODataRoute("Machines")]
[EnableQuery]
public IQueryable<Machine> FilterMachines(ODataQueryOptions opts)
{
// Get the default query with security applied
var expression = GetQuery();
// TODO: apply any additional queries specific to this endpoint, if there are any
return expression;
}
[HttpGet]
[ODataRoute("Machine")]
[EnableQuery] // so we can still apply $select and $expand
[HttpGet]
public SingleResult<Machine> GetMachine([FromODataUri] int key)
{
// Get the default query with security applied
var query = GetQuery();
// Now filter for just this item by id
query = query.Where(m => m.Id == key);
return SingleResult.Create(query);
}
[HttpGet]
[ODataRoute("MachinesThatNeedService")]
[EnableQuery]
internal IQueryable<Machine> GetMachinesServiceDue(ODataQueryOptions opts)
{
// Get the default query with security applied
var query = GetQuery();
// apply the specific filter for this endpoint
var lastValidServiceDate = DateTimeOffset.Now.Add(-TimeSpan.FromDays(60));
query = query.Where(m => m.LastService < lastValidServiceDate);
return query;
}
You can use OWIN middelware to enter in the pipe of the request.
You will have a function with HTTP request and you can decide to accept or reject the request.
Function to implement is like this:
public async override Task Invoke(IOwinContext context)
{
// here do your check!!
if(isValid)
{
await Next.Invoke(context);
}
Console.WriteLine("End Request");
}
I've found out today that for my site i could use a SiteMap provider which i've downloaded from Github for the MVC3, as my web application is MVC3.
Situation is following, my application is multilingual. I have a separate Library which contains all the resources. This Library is then added to my current project and everywhere where i need i use those resource files.
Now i've implemented the site map provider:
<mvcSiteMapNode title="$resources:Base,Home" controller="Home" action="Index" enableLocalization="true">
<mvcSiteMapNode title="Search" controller="Search" action="Index"/>
<mvcSiteMapNode title="Contact" controller="Contact" action="Index"/>
<mvcSiteMapNode title="About" controller="Home" action="About"/>
</mvcSiteMapNode>
But when i run it i get an error because it cannot find the resource with the key home. I think it is due to the fact that is outside of the application but in a separate Library.
How do i point to the resource file then, which is located din separate project?
The approach I would take would be to switch to external DI and then implement a custom IStringLocalizer class that can read the resources from another assembly. Here is a working example. I have created a demo application on GitHub as well.
using System;
using System.Collections.Specialized;
using System.Resources;
namespace MvcSiteMapProvider.Globalization
{
public class ResourceManagerStringLocalizer
: IStringLocalizer
{
public ResourceManagerStringLocalizer(
ResourceManager resourceManager
)
{
if (resourceManager == null)
throw new ArgumentNullException("resourceManager");
this.resourceManager = resourceManager;
}
protected readonly ResourceManager resourceManager;
/// <summary>
/// Gets the localized text for the supplied attributeName.
/// </summary>
/// <param name="attributeName">The name of the attribute (as if it were in the original XML file).</param>
/// <param name="value">The current object's value of the attribute.</param>
/// <param name="enableLocalization">True if localization has been enabled, otherwise false.</param>
/// <param name="classKey">The resource key from the ISiteMap class.</param>
/// <param name="implicitResourceKey">The implicit resource key.</param>
/// <param name="explicitResourceKeys">A <see cref="T:System.Collections.Specialized.NameValueCollection"/> containing the explicit resource keys.</param>
/// <returns></returns>
public virtual string GetResourceString(string attributeName, string value, bool enableLocalization, string classKey, string implicitResourceKey, NameValueCollection explicitResourceKeys)
{
if (attributeName == null)
{
throw new ArgumentNullException("attributeName");
}
if (enableLocalization)
{
string result = string.Empty;
if (explicitResourceKeys != null)
{
string[] values = explicitResourceKeys.GetValues(attributeName);
if ((values == null) || (values.Length <= 1))
{
result = value;
}
else if (this.resourceManager.BaseName.Equals(values[0]))
{
try
{
result = this.resourceManager.GetString(values[1]);
}
catch (MissingManifestResourceException)
{
if (!string.IsNullOrEmpty(value))
{
result = value;
}
}
}
}
if (!string.IsNullOrEmpty(result))
{
return result;
}
}
if (!string.IsNullOrEmpty(value))
{
return value;
}
return string.Empty;
}
}
}
Then you can inject it into your DI configuration module (StructureMap example shown, but any DI container will do).
First of all, you need to specify not to register the IStringLocalizer interface automatically by adding it to the excludeTypes variable.
var excludeTypes = new Type[] {
// Use this array to add types you wish to explicitly exclude from convention-based
// auto-registration. By default all types that either match I[TypeName] = [TypeName] or
// I[TypeName] = [TypeName]Adapter will be automatically wired up as long as they don't
// have the [ExcludeFromAutoRegistrationAttribute].
//
// If you want to override a type that follows the convention, you should add the name
// of either the implementation name or the interface that it inherits to this list and
// add your manual registration code below. This will prevent duplicate registrations
// of the types from occurring.
// Example:
// typeof(SiteMap),
// typeof(SiteMapNodeVisibilityProviderStrategy)
typeof(IStringLocalizer)
};
Then provide an explicit registration of the ResourceManagerStringLocalizer (and its dependencies) instead.
// Configure localization
// Fully qualified namespace.resourcefile (.resx) name without the extension
string resourceBaseName = "SomeAssembly.Resources.Resource1";
// A reference to the assembly where your resources reside.
Assembly resourceAssembly = typeof(SomeAssembly.Class1).Assembly;
// Register the ResourceManager (note that this is application wide - if you are
// using ResourceManager in your DI setup already you may need to use a named
// instance or SmartInstance to specify a specific object to inject)
this.For<ResourceManager>().Use(() => new ResourceManager(resourceBaseName, resourceAssembly));
// Register the ResourceManagerStringLocalizer (uses the ResourceManger)
this.For<IStringLocalizer>().Use<ResourceManagerStringLocalizer>();
Then it is just a matter of specifying the resources appropriately. You need to start them with the Base Name (in this case SomeAssembly.Resources.Resource1), and then specify the key of the resource as the second argument.
<mvcSiteMapNode title="$resources:SomeAssembly.Resources.Resource1,ContactTitle" controller="Home" action="Contact"/>
Note that getting the BaseName right is the key to making it work. See the following MSDN documentation: http://msdn.microsoft.com/en-us/library/yfsz7ac5(v=vs.110).aspx
I think it should be like this:
Assuming your:
ProjectName=MyProject
Folder which contains resources:LanguageFiles
Class in which resources are defined:Messaging
then.
It should be like this I believe..I am not sure but may be it works for you:
`title=#MyProject.LanguageFiles.Messaging.Home'
(assuming Home is the resource defined in the Messaging class)
While trying to implement EF Migrations in my project I am stuck at one place.
EF Code First MigrateDatabaseToLatestVersion accepts connection string Name from config.
In my case database name get known at Runtime (User selects it from dropdown).
Just the way DbContext either accepts, ConnectionString or connectionString Name in it's constructor, "MigrateDatabaseToLatestVersion" does not accept the same
System.Data.Entity.Database.SetInitializer
(new MigrateDatabaseToLatestVersion<SrcDbContext, SRC.DomainModel.ORMapping.Migrations.Configuration>(connString));
Is there any other way to achieve this?
Thank you all. I did checkout the EF code from codeplex, and inherited my own class after understanding their source code. Here is the solution which I opted :-
public class MigrateDbToLatestInitializerConnString<TContext, TMigrationsConfiguration> : IDatabaseInitializer<TContext>
where TContext : DbContext
where TMigrationsConfiguration : DbMigrationsConfiguration<TContext>, new()
{
private readonly DbMigrationsConfiguration config;
/// <summary>
/// Initializes a new instance of the MigrateDatabaseToLatestVersion class.
/// </summary>
public MigrateDbToLatestInitializerConnString()
{
config = new TMigrationsConfiguration();
}
/// <summary>
/// Initializes a new instance of the MigrateDatabaseToLatestVersion class that will
/// use a specific connection string from the configuration file to connect to
/// the database to perform the migration.
/// </summary>
/// <param name="connectionString"> connection string to use for migration. </param>
public MigrateDbToLatestInitializerConnString(string connectionString)
{
config = new TMigrationsConfiguration
{
TargetDatabase = new DbConnectionInfo(connectionString, "System.Data.SqlClient")
};
}
public void InitializeDatabase(TContext context)
{
if (context == null)
{
throw new ArgumentException("Context passed to InitializeDatabase can not be null");
}
var migrator = new DbMigrator(config);
migrator.Update();
}
}
public static class DatabaseHelper
{
/// <summary>
/// This method will create data base for given parameters supplied by caller.
/// </summary>
/// <param name="serverName">Name of the server where database has to be created</param>
/// <param name="databaseName">Name of database</param>
/// <param name="userName">SQL user name</param>
/// <param name="password">SQL password</param>
/// <returns>void</returns>
public static bool CreateDb(string serverName, string databaseName, string userName, string password)
{
bool integratedSecurity = !(!string.IsNullOrEmpty(userName) || !string.IsNullOrEmpty(password));
var builder = new System.Data.SqlClient.SqlConnectionStringBuilder
{
DataSource = serverName,
UserID = userName,
Password = password,
InitialCatalog = databaseName,
IntegratedSecurity = integratedSecurity,
};
var db = new SrcDbContext(builder.ConnectionString);
var dbInitializer = new MigrateDbToLatestInitializerConnString<SrcDbContext, SRC.DomainModel.ORMapping.Migrations.Configuration>(builder.ConnectionString);
//following uses strategy to "CreateIfNotExist<>"
dbInitializer.InitializeDatabase(db);
return true;
}
}
You can make the MigrateDatabaseToLatestVersion initializer to use the connection string that was used by the context that triggered the migration in the first place.
This is done by passing useSuppliedContext: true to the MigrateDatabaseToLatestVersion constructor as described in the docs. In your case:
Database.SetInitializer(new MigrateDatabaseToLatestVersion<SrcDbContext, SRC.DomainModel.ORMapping.Migrations.Configuration>(useSuppliedContext: true));
What context is this running under? Website or Desktop App?
Under website, doing that is not a good idea. The database initializing strategy set against the type of context. So different connection strings with same type of context will override each other's init strategy.
If Desktop App, maybe include an extra utility to switch between database?
Either way I don't recommend doing this, but if you really want to do what you mentioned, it looks like you have to hack it.
using (var context = new DbContext("<Your connection string right in here>"))
{
var constructors = typeof (DbMigrator).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
var hackedDbMigrator = constructors[0].Invoke(new object[] { new Configuration(), context }) as DbMigrator;
hackedDbMigrator.Update();
}
The is a issue with Migrations call the DbContext dervied class with parameter. Once this is solved it should work. see here for a sample solution.
EntityFramework code-first custom connection string and migrations