In my project there is several modules. Source files for them are generated by heatdirectory.
<HeatDirectory DirectoryRefId="ServerAdminService" OutputFile="Source\ServerAdminServiceSource.wxs" Transforms="Filter.xsl" Directory="..\..\Server\ServerServiceManager\bin\Debug\" PreprocessorVariable="var.ServerAdminServicePath" ComponentGroupName="ServerAdminServiceGroup" ToolPath="$(WixToolPath)" SuppressCom="true" SuppressFragments="true" SuppressRegistry="true" SuppressRootDirectory="true" AutoGenerateGuids="true" GenerateGuidsNow="false">
</HeatDirectory>
<HeatDirectory DirectoryRefId="ServerAdminService" OutputFile="Source\ServerAdminClientSource.wxs" Transforms="Filter.xsl" Directory="..\Setup\C24.ServerAdmin.UI\bin\Debug\" PreprocessorVariable="var.ServerAdminClientPath" ComponentGroupName="ServerAdminClientGroup" ToolPath="$(WixToolPath)" SuppressCom="true" SuppressFragments="true" SuppressRegistry="true" SuppressRootDirectory="true" AutoGenerateGuids="true" GenerateGuidsNow="false">
</HeatDirectory>
It work fine. I need to install them in one directory. But they use several libraries, which presents in both modules, and after generating source files consist the component with duplicate ID. Actually i don't know what to do. Does somebody have an idea?
I had the exact same issue.
I had solved it by creating a custom build task to run after the HeatDirectory task to append a suffix to the Id attribute.
<AddSuffixToHeatDirectory File="ReportFiles.Generated.wxs" Suffix="_r" />
The AddSuffixToHeatDirectory task is as such
public class AddSuffixToHeatDirectory : Task
{
public override bool Execute()
{
bool result = true;
Log.LogMessage("Opening file '{0}'.", File);
var document = XElement.Load(File);
var defaultNamespace = GetDefaultNamespace(document);
AddSuffixToAttribute(document, defaultNamespace, "Component", "Id");
AddSuffixToAttribute(document, defaultNamespace, "File", "Id");
AddSuffixToAttribute(document, defaultNamespace, "ComponentRef", "Id");
AddSuffixToAttribute(document, defaultNamespace, "Directory", "Id");
var files = (from x in document.Descendants(defaultNamespace.GetName("File")) select x).ToList();
Log.LogMessage("Saving file '{0}'.", File);
document.Save(File);
return result;
}
private void AddSuffixToAttribute(XElement xml, XNamespace defaultNamespace, string elementName, string attributeName)
{
var items = (from x in xml.Descendants(defaultNamespace.GetName(elementName)) select x).ToList();
foreach (var item in items)
{
var attribute = item.Attribute(attributeName);
attribute.Value = string.Format("{0}{1}", attribute.Value, Suffix);
}
}
private XNamespace GetDefaultNamespace(XElement root)
{
// I pieced together this query from hanselman's post.
// http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx
//
// Basically I'm just getting the namespace that doesn't have a localname.
var result = root.Attributes()
.Where(a => a.IsNamespaceDeclaration)
.GroupBy(a => a.Name.Namespace == XNamespace.None ? String.Empty : a.Name.LocalName, a => XNamespace.Get(a.Value))
.ToDictionary(g => g.Key, g => g.First());
return result[string.Empty];
}
/// <summary>
/// File to modify.
/// </summary>
[Required]
public string File { get; set; }
/// <summary>
/// Suffix to append.
/// </summary>
[Required]
public string Suffix { get; set; }
}
Hope that helps. I'm still using this method today and I've not done the legwork to look into a Transform or extending HeatDirectory instead.
Related
I have a controller that requests a model containing an IFormFile as one of it's properties. For the request description, the Swagger UI (I'm using Swashbuckle and OpenApi 3.0 for .NET Core) lists the type of the file property as type object. Is there some way to make the Swagger UI denote the exact type and it's JSON representation to help the client?
The controller requesting the model looks as follows.
[HttpPost]
[Consumes("multipart/form-data")
public async Task<IActionResult> CreateSomethingAndUploadFile ([FromForm]RequestModel model)
{
// do something
}
And the model is defined as below:
public class AssetCreationModel
{
[Required}
public string Filename { get; set; }
[Required]
public IFormFile File { get; set; }
}
We've been exploring this issue today. If you add the following to your startup it will convert IFormFile to the correct type
services.AddSwaggerGen(c => {
c.SchemaRegistryOptions.CustomTypeMappings.Add(typeof(IFormFile), () => new Schema() { Type = "file", Format = "binary"});
});
Also see the following article on file upload in .net core
https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-2.1
This problem was already tackled in the following github issue/thread.
This improvement was already merged into Swashbuckle.AspNetCore master (as per 10/30/2018), but i don't expect that to be available as a package soon.
There are simple solutions if you only have a IFormFile as a parameter.
public async Task UploadFile(IFormFile filePayload){}
For simple case you can take a look at the following answer.
For complicated cases like container cases, you can take a look at the following answer.
internal class FormFileOperationFilter : IOperationFilter
{
private struct ContainerParameterData
{
public readonly ParameterDescriptor Parameter;
public readonly PropertyInfo Property;
public string FullName => $"{Parameter.Name}.{Property.Name}";
public string Name => Property.Name;
public ContainerParameterData(ParameterDescriptor parameter, PropertyInfo property)
{
Parameter = parameter;
Property = property;
}
}
private static readonly ImmutableArray<string> iFormFilePropertyNames =
typeof(IFormFile).GetTypeInfo().DeclaredProperties.Select(p => p.Name).ToImmutableArray();
public void Apply(Operation operation, OperationFilterContext context)
{
var parameters = operation.Parameters;
if (parameters == null)
return;
var #params = context.ApiDescription.ActionDescriptor.Parameters;
if (parameters.Count == #params.Count)
return;
var formFileParams =
(from parameter in #params
where parameter.ParameterType.IsAssignableFrom(typeof(IFormFile))
select parameter).ToArray();
var iFormFileType = typeof(IFormFile).GetTypeInfo();
var containerParams =
#params.Select(p => new KeyValuePair<ParameterDescriptor, PropertyInfo[]>(
p, p.ParameterType.GetProperties()))
.Where(pp => pp.Value.Any(p => iFormFileType.IsAssignableFrom(p.PropertyType)))
.SelectMany(p => p.Value.Select(pp => new ContainerParameterData(p.Key, pp)))
.ToImmutableArray();
if (!(formFileParams.Any() || containerParams.Any()))
return;
var consumes = operation.Consumes;
consumes.Clear();
consumes.Add("application/form-data");
if (!containerParams.Any())
{
var nonIFormFileProperties =
parameters.Where(p =>
!(iFormFilePropertyNames.Contains(p.Name)
&& string.Compare(p.In, "formData", StringComparison.OrdinalIgnoreCase) == 0))
.ToImmutableArray();
parameters.Clear();
foreach (var parameter in nonIFormFileProperties) parameters.Add(parameter);
foreach (var parameter in formFileParams)
{
parameters.Add(new NonBodyParameter
{
Name = parameter.Name,
//Required = , // TODO: find a way to determine
Type = "file"
});
}
}
else
{
var paramsToRemove = new List<IParameter>();
foreach (var parameter in containerParams)
{
var parameterFilter = parameter.Property.Name + ".";
paramsToRemove.AddRange(from p in parameters
where p.Name.StartsWith(parameterFilter)
select p);
}
paramsToRemove.ForEach(x => parameters.Remove(x));
foreach (var parameter in containerParams)
{
if (iFormFileType.IsAssignableFrom(parameter.Property.PropertyType))
{
var originalParameter = parameters.FirstOrDefault(param => param.Name == parameter.Name);
parameters.Remove(originalParameter);
parameters.Add(new NonBodyParameter
{
Name = parameter.Name,
Required = originalParameter.Required,
Type = "file",
In = "formData"
});
}
}
}
}
}
You need to look into how you can add some/an OperationFilter that is suitable for your case.
I am currently using Fluent Validation instead of Data Annotations for my Web api and using swagger for API documentation. Fluent validation rules are not reflected in swagger model as i am unable to configure fluent validation rules with swagger schema filter.
This Blog has a good explanation for using it with ASP.net MVC. but i am unable to configure it to use it in ASP.net Core.
So far i have tried the following code but i am unable to get validator type.
services.AddSwaggerGen(options => options.SchemaFilter<AddFluentValidationRules>());
public class AddFluentValidationRules : ISchemaFilter
{
public void Apply(Schema model, SchemaFilterContext context)
{
model.Required = new List<string>();
var validator = GetValidator(type); // How?
var validatorDescriptor = validator.CreateDescriptor();
foreach (var key in model.Properties.Keys)
{
foreach (var propertyValidator in validatorDescriptor.GetValidatorsForMember(key))
{
// Add to model properties as in blog
}
}
}
}
I've created github project and nuget package based on Mujahid Daud Khan answer. I made redesign to support extensibility and supported other validators.
github: https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation
nuget: https://www.nuget.org/packages/MicroElements.Swashbuckle.FluentValidation
Note: For WebApi see: https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation.WebApi
Supported validators
INotNullValidator (NotNull)
INotEmptyValidator (NotEmpty)
ILengthValidator (Length, MinimumLength, MaximumLength, ExactLength)
IRegularExpressionValidator (Email, Matches)
IComparisonValidator (GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual)
IBetweenValidator (InclusiveBetween, ExclusiveBetween)
Usage
1. Reference packages in your web project:
<PackageReference Include="FluentValidation.AspNetCore" Version="7.5.2" />
<PackageReference Include="MicroElements.Swashbuckle.FluentValidation" Version="0.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="2.3.0" />
2. Change Startup.cs
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
// Adds fluent validators to Asp.net
.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<CustomerValidator>());
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
// Adds fluent validation rules to swagger
c.AddFluentValidationRules();
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app
.UseMvc()
// Adds swagger
.UseSwagger();
// Adds swagger UI
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
}
Swagger Sample model and validator
public class Sample
{
public string PropertyWithNoRules { get; set; }
public string NotNull { get; set; }
public string NotEmpty { get; set; }
public string EmailAddress { get; set; }
public string RegexField { get; set; }
public int ValueInRange { get; set; }
public int ValueInRangeExclusive { get; set; }
}
public class SampleValidator : AbstractValidator<Sample>
{
public SampleValidator()
{
RuleFor(sample => sample.NotNull).NotNull();
RuleFor(sample => sample.NotEmpty).NotEmpty();
RuleFor(sample => sample.EmailAddress).EmailAddress();
RuleFor(sample => sample.RegexField).Matches(#"(\d{4})-(\d{2})-(\d{2})");
RuleFor(sample => sample.ValueInRange).GreaterThanOrEqualTo(5).LessThanOrEqualTo(10);
RuleFor(sample => sample.ValueInRangeExclusive).GreaterThan(5).LessThan(10);
}
}
Feel free to add issues!
After searching i have finally figured out that i needed to IValidationFactory for validator instance.
public class AddFluentValidationRules : ISchemaFilter
{
private readonly IValidatorFactory _factory;
/// <summary>
/// Default constructor with DI
/// </summary>
/// <param name="factory"></param>
public AddFluentValidationRules(IValidatorFactory factory)
{
_factory = factory;
}
/// <summary>
/// </summary>
/// <param name="model"></param>
/// <param name="context"></param>
public void Apply(Schema model, SchemaFilterContext context)
{
// use IoC or FluentValidatorFactory to get AbstractValidator<T> instance
var validator = _factory.GetValidator(context.SystemType);
if (validator == null) return;
if (model.Required == null)
model.Required = new List<string>();
var validatorDescriptor = validator.CreateDescriptor();
foreach (var key in model.Properties.Keys)
{
foreach (var propertyValidator in validatorDescriptor
.GetValidatorsForMember(ToPascalCase(key)))
{
if (propertyValidator is NotNullValidator
|| propertyValidator is NotEmptyValidator)
model.Required.Add(key);
if (propertyValidator is LengthValidator lengthValidator)
{
if (lengthValidator.Max > 0)
model.Properties[key].MaxLength = lengthValidator.Max;
model.Properties[key].MinLength = lengthValidator.Min;
}
if (propertyValidator is RegularExpressionValidator expressionValidator)
model.Properties[key].Pattern = expressionValidator.Expression;
// Add more validation properties here;
}
}
}
/// <summary>
/// To convert case as swagger may be using lower camel case
/// </summary>
/// <param name="inputString"></param>
/// <returns></returns>
private static string ToPascalCase(string inputString)
{
// If there are 0 or 1 characters, just return the string.
if (inputString == null) return null;
if (inputString.Length < 2) return inputString.ToUpper();
return inputString.Substring(0, 1).ToUpper() + inputString.Substring(1);
}
}
and add this class to swaggerGen options
options.SchemaFilter<AddFluentValidationRules>();
Install Nuget package: MicroElements.Swashbuckle.FluentValidation
Add to ConfigureServices:
services.AddFluentValidationRulesToSwagger();
I'm trying to have LESS files in my web project, and have the MVC 4 bundling functionality call into the dotLess library to turn the LESS into CSS, then minify the result and give it to the browser.
I found an example on the ASP.NET site (under the heading LESS, CoffeeScript, SCSS, Sass Bundling.). This has given me a LessTransform class that looks like this:
public class LessTransform : IBundleTransform
{
public void Process(BundleContext context, BundleResponse response)
{
response.Content = dotless.Core.Less.Parse(response.Content);
response.ContentType = "text/css";
}
}
and this line in my BundleConfig class:
bundles.Add(new Bundle(
"~/Content/lessTest",
new LessTransform(),
new CssMinify()).Include("~/Content/less/test.less"));
finally I have the following line in my _Layout.cshtml, in the <head>:
#Styles.Render("~/Content/lessTest")
If I have the site in debug mode, this is rendered to the browser:
<link href="/Content/less/test.less" rel="stylesheet"/>
The rules in the .less file are applied, and following that link shows that the LESS has been correctly transformed into CSS.
However, if I put the site into release mode, this is rendered out:
<link href="/Content/less?v=lEs-HID6XUz3s2qkJ35Lvnwwq677wTaIiry6fuX8gz01" rel="stylesheet"/>
The rules in the .less file are not applied, because following the link gives a 404 error from IIS.
So it seems that something is going wrong with the bundling. How do I get this to work in release mode, or how do I find out what exactly is going wrong?
As a complement to the accepted answer, I created a LessBundle class, which is the Less eqivalent of the StyleBundle class.
LessBundle.cs code is:
using System.Web.Optimization;
namespace MyProject
{
public class LessBundle : Bundle
{
public LessBundle(string virtualPath) : base(virtualPath, new IBundleTransform[] {new LessTransform(), new CssMinify()})
{
}
public LessBundle(string virtualPath, string cdnPath)
: base(virtualPath, cdnPath, new IBundleTransform[] { new LessTransform(), new CssMinify() })
{
}
}
}
Usage is similar to the StyleBundle class, specifying a LESS file instead of a CSS file.
Add the following to your BundleConfig.RegisterBundles(BundleCollection) method:
bundles.Add(new LessBundle("~/Content/less").Include(
"~/Content/MyStyles.less"));
Update
This method works fine with optimization switched off, but I ran into some minor problems (with CSS resource paths) when optimization was switched on. After an hour researching the issue I discovered that I have reinvented the wheel...
If you do want the LessBundle functionality I describe above, check out System.Web.Optimization.Less.
The NuGet package can be found here.
Edited 12/8/2019 This is no longer an acceptable answer to this issue as there have been breaking changes in ASP.NET over the years. There are other answers further down that have modified this code or supplied other answers to help you fix this issue.
It appears that the dotless engine needs to know the path of the currently processed bundle file to resolve #import paths. If you run the process code that you have above, the result of the dotless.Core.Less.Parse() is an empty string when the .less file being parsed has other less files imported.
Ben Foster's response here will fix that by reading the imported files first:
Import Files and DotLess
Change your LessTransform file as follows:
public class LessTransform : IBundleTransform
{
public void Process(BundleContext context, BundleResponse bundle)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (bundle == null)
{
throw new ArgumentNullException("bundle");
}
context.HttpContext.Response.Cache.SetLastModifiedFromFileDependencies();
var lessParser = new Parser();
ILessEngine lessEngine = CreateLessEngine(lessParser);
var content = new StringBuilder(bundle.Content.Length);
var bundleFiles = new List<FileInfo>();
foreach (var bundleFile in bundle.Files)
{
bundleFiles.Add(bundleFile);
SetCurrentFilePath(lessParser, bundleFile.FullName);
string source = File.ReadAllText(bundleFile.FullName);
content.Append(lessEngine.TransformToCss(source, bundleFile.FullName));
content.AppendLine();
bundleFiles.AddRange(GetFileDependencies(lessParser));
}
if (BundleTable.EnableOptimizations)
{
// include imports in bundle files to register cache dependencies
bundle.Files = bundleFiles.Distinct();
}
bundle.ContentType = "text/css";
bundle.Content = content.ToString();
}
/// <summary>
/// Creates an instance of LESS engine.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
private ILessEngine CreateLessEngine(Parser lessParser)
{
var logger = new AspNetTraceLogger(LogLevel.Debug, new Http());
return new LessEngine(lessParser, logger, true, false);
}
/// <summary>
/// Gets the file dependencies (#imports) of the LESS file being parsed.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
/// <returns>An array of file references to the dependent file references.</returns>
private IEnumerable<FileInfo> GetFileDependencies(Parser lessParser)
{
IPathResolver pathResolver = GetPathResolver(lessParser);
foreach (var importPath in lessParser.Importer.Imports)
{
yield return new FileInfo(pathResolver.GetFullPath(importPath));
}
lessParser.Importer.Imports.Clear();
}
/// <summary>
/// Returns an <see cref="IPathResolver"/> instance used by the specified LESS lessParser.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
private IPathResolver GetPathResolver(Parser lessParser)
{
var importer = lessParser.Importer as Importer;
var fileReader = importer.FileReader as FileReader;
return fileReader.PathResolver;
}
/// <summary>
/// Informs the LESS parser about the path to the currently processed file.
/// This is done by using a custom <see cref="IPathResolver"/> implementation.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
/// <param name="currentFilePath">The path to the currently processed file.</param>
private void SetCurrentFilePath(Parser lessParser, string currentFilePath)
{
var importer = lessParser.Importer as Importer;
if (importer == null)
throw new InvalidOperationException("Unexpected dotless importer type.");
var fileReader = importer.FileReader as FileReader;
if (fileReader == null || !(fileReader.PathResolver is ImportedFilePathResolver))
{
fileReader = new FileReader(new ImportedFilePathResolver(currentFilePath));
importer.FileReader = fileReader;
}
}
}
public class ImportedFilePathResolver : IPathResolver
{
private string currentFileDirectory;
private string currentFilePath;
public ImportedFilePathResolver(string currentFilePath)
{
if (string.IsNullOrEmpty(currentFilePath))
{
throw new ArgumentNullException("currentFilePath");
}
CurrentFilePath = currentFilePath;
}
/// <summary>
/// Gets or sets the path to the currently processed file.
/// </summary>
public string CurrentFilePath
{
get { return currentFilePath; }
set
{
currentFilePath = value;
currentFileDirectory = Path.GetDirectoryName(value);
}
}
/// <summary>
/// Returns the absolute path for the specified improted file path.
/// </summary>
/// <param name="filePath">The imported file path.</param>
public string GetFullPath(string filePath)
{
if (filePath.StartsWith("~"))
{
filePath = VirtualPathUtility.ToAbsolute(filePath);
}
if (filePath.StartsWith("/"))
{
filePath = HostingEnvironment.MapPath(filePath);
}
else if (!Path.IsPathRooted(filePath))
{
filePath = Path.GetFullPath(Path.Combine(currentFileDirectory, filePath));
}
return filePath;
}
}
The accepted answer does not work with recent changes to ASP.NET, so is no longer correct.
I've fixed the source in the accepted answer:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web.Hosting;
using System.Web.Optimization;
using dotless.Core;
using dotless.Core.Abstractions;
using dotless.Core.Importers;
using dotless.Core.Input;
using dotless.Core.Loggers;
using dotless.Core.Parser;
namespace Web.App_Start.Bundles
{
public class LessTransform : IBundleTransform
{
public void Process(BundleContext context, BundleResponse bundle)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (bundle == null)
{
throw new ArgumentNullException("bundle");
}
context.HttpContext.Response.Cache.SetLastModifiedFromFileDependencies();
var lessParser = new Parser();
ILessEngine lessEngine = CreateLessEngine(lessParser);
var content = new StringBuilder(bundle.Content.Length);
var bundleFiles = new List<BundleFile>();
foreach (var bundleFile in bundle.Files)
{
bundleFiles.Add(bundleFile);
var name = context.HttpContext.Server.MapPath(bundleFile.VirtualFile.VirtualPath);
SetCurrentFilePath(lessParser, name);
using (var stream = bundleFile.VirtualFile.Open())
using (var reader = new StreamReader(stream))
{
string source = reader.ReadToEnd();
content.Append(lessEngine.TransformToCss(source, name));
content.AppendLine();
}
bundleFiles.AddRange(GetFileDependencies(lessParser));
}
if (BundleTable.EnableOptimizations)
{
// include imports in bundle files to register cache dependencies
bundle.Files = bundleFiles.Distinct();
}
bundle.ContentType = "text/css";
bundle.Content = content.ToString();
}
/// <summary>
/// Creates an instance of LESS engine.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
private ILessEngine CreateLessEngine(Parser lessParser)
{
var logger = new AspNetTraceLogger(LogLevel.Debug, new Http());
return new LessEngine(lessParser, logger, true, false);
}
/// <summary>
/// Gets the file dependencies (#imports) of the LESS file being parsed.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
/// <returns>An array of file references to the dependent file references.</returns>
private IEnumerable<BundleFile> GetFileDependencies(Parser lessParser)
{
IPathResolver pathResolver = GetPathResolver(lessParser);
foreach (var importPath in lessParser.Importer.Imports)
{
yield return
new BundleFile(pathResolver.GetFullPath(importPath),
HostingEnvironment.VirtualPathProvider.GetFile(importPath));
}
lessParser.Importer.Imports.Clear();
}
/// <summary>
/// Returns an <see cref="IPathResolver"/> instance used by the specified LESS lessParser.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
private IPathResolver GetPathResolver(Parser lessParser)
{
var importer = lessParser.Importer as Importer;
var fileReader = importer.FileReader as FileReader;
return fileReader.PathResolver;
}
/// <summary>
/// Informs the LESS parser about the path to the currently processed file.
/// This is done by using a custom <see cref="IPathResolver"/> implementation.
/// </summary>
/// <param name="lessParser">The LESS parser.</param>
/// <param name="currentFilePath">The path to the currently processed file.</param>
private void SetCurrentFilePath(Parser lessParser, string currentFilePath)
{
var importer = lessParser.Importer as Importer;
if (importer == null)
throw new InvalidOperationException("Unexpected dotless importer type.");
var fileReader = importer.FileReader as FileReader;
if (fileReader == null || !(fileReader.PathResolver is ImportedFilePathResolver))
{
fileReader = new FileReader(new ImportedFilePathResolver(currentFilePath));
importer.FileReader = fileReader;
}
}
}
}
Please note one known issue with this code as is is that LESS #imports must use their full paths, i.e. you must use #import "~/Areas/Admin/Css/global.less"; instead of #import "global.less";.
Looks like this works - I changed the Process method to iterate over the file collection:
public void Process(BundleContext context, BundleResponse response)
{
var builder = new StringBuilder();
foreach (var fileInfo in response.Files)
{
using (var reader = fileInfo.OpenText())
{
builder.Append(dotless.Core.Less.Parse(reader.ReadToEnd()));
}
}
response.Content = builder.ToString();
response.ContentType = "text/css";
}
This breaks if there are any #import statements in your less files though, in this case you have to do a bit more work, like this: https://gist.github.com/chrisortman/2002958
Already some great answers, here's a very simple solution I found for myself when trying to add MVC bundles that regard less files.
After creating your less file (for example, test.less), right click on it and under Web Compiler (get it here) option, select Compile File.
This generates the resulting css file from your less one, and also its minified version. (test.css and test.min.css).
On your bundle, just refer to the generated css file
style = new StyleBundle("~/bundles/myLess-styles")
.Include("~/Content/css/test.css", new CssRewriteUrlTransform());
bundles.Add(style);
And on your view, reference that bundle:
#Styles.Render("~/bundles/myLess-styles")
It should just work fine.
Here's the (edited) domain class:
public class Patient : IntegerKeyEntity
{
...
public virtual string LastName
{
get
{
return Encoding.Unicode.GetString(encryptionService.Decrypt(LastNameEncrypted));
}
set
{
LastNameEncrypted = encryptionService.Encrypt(value);
}
}
/// <summary>
/// Contains the encrypted last name. Access via LastName for unencrypted version.
/// </summary>
public virtual byte[] LastNameEncrypted { get; set; }
...
}
Here's the Fluent mapping:
public class PatientMap : ClassMap<Patient>
{
public PatientMap()
{
...
Map(x => x.LastNameEncrypted, "LastName")
.Not.Nullable();
...
}
}
I don't map LastName in Patient. The table has a LastName column (a varbinary) and no LastNameEncrypted column.
Here's the query:
public virtual IQueryable<TResult> SatisfyingElementsFrom(IQueryable<T> candidates, int start, int limit, string sort, string dir)
{
if (this.MatchingCriteria != null)
{
return candidates.Where(this.MatchingCriteria).OrderBy(sort + " " + dir).Skip(start).Take(limit).ToList().ConvertAll(this.ResultMap).AsQueryable();
}
return candidates.ToList().ConvertAll(this.ResultMap).AsQueryable();
}
The return (inside the if block) is where the error is triggered. The error says "Could not resolve property LastName of Patient".
I am using NHibernate (v2.1.2.4000), Fluent NHibernate (v1.1.0.685) and NHibernate.Linq (v1.1.0.1001). I cannot update these DLLs.
Is it because I don't have a mapping for Patient.LastName? I don't have or need one. If that is the issue, how do I map/tell Fluent NHibernate to ignore that property?
PS: I am not using AutoMapping, only explicit mappings. They are loaded as follows. In my application, only cfg (an NHibernate.Cfg.Configuration object) and mappingAssemblies (which points to the one DLL with the mappings) have a value.
private static ISessionFactory CreateSessionFactoryFor(string[] mappingAssemblies, AutoPersistenceModel autoPersistenceModel, Configuration cfg, IPersistenceConfigurer persistenceConfigurer)
{
FluentConfiguration fluentConfiguration = Fluently.Configure(cfg);
if (persistenceConfigurer != null)
{
fluentConfiguration.Database(persistenceConfigurer);
}
fluentConfiguration.Mappings(m =>
{
foreach (var mappingAssembly in mappingAssemblies)
{
var assembly = Assembly.LoadFrom(MakeLoadReadyAssemblyName(mappingAssembly));
m.HbmMappings.AddFromAssembly(assembly);
m.FluentMappings.AddFromAssembly(assembly).Conventions.AddAssembly(assembly);
}
if (autoPersistenceModel != null)
{
m.AutoMappings.Add(autoPersistenceModel);
}
});
return fluentConfiguration.BuildSessionFactory();
}
This error happens when you do the query. Looking to your code I see only one thing that might cause the problem - that is MatchingCriteria:
return candidates.Where(this.MatchingCriteria)...
What type is stored in this.MatchingCriteria?
Try to replace it with inline conditions: in
..Where(<put_inline_criteria_here>)..
How does one call a class static method from msbuild and store its results in a list?
EDIT: Okay, let me explain a bit further. I am using sandcastle help file builder to generate documentation for my application. One of the requirements is that you must specify the documentation sources as follows:
<DocumentationSources>
<DocumentationSource sourceFile="$(MSBuildProjectDirectory)\..\src\myApp\bin\Debug\myApp.exe" xmlns="" />
<DocumentationSource sourceFile="$(MSBuildProjectDirectory)\..\src\myApp\bin\Debug\myApp.xml" xmlns="" />
</DocumentationSources>
Sandcastle Help File Builder comes with a utils assembly that has a way of retrieving all dll and xml files from a specified directory. I want to call the method from this assembly and store its result as a list of <DocumentationSource>. This is a static method which returns Collection<string>
Custom Tasks are great but potential overkill if you want to do something simple. I believe Draco is asking about the Property Functions feature in MSBuild 4.
An example of setting a property by using a static function (ripped directly from above page):
<Today>$([System.DateTime]::Now)</Today>
And to call a static function on parameters:
$([Class]:: Property.Method(Parameters))
Or, perhaps you want something crazier like inline tasks.
Create a custom task calling that static method and returning an array of ITaskItem.
Or
You could try using the MSBuild Extension Pack Assembly.Invoke :
<PropertyGroup>
<StaticMethodAssemblyPath>path</StaticMethodAssemblyPath>
</PropertyGroup>
<MSBuild.ExtensionPack.Framework.Assembly TaskAction="Invoke"
NetArguments="#(ArgsM)"
NetClass="StaticMethodClassName"
NetMethod="StaticMethodName"
NetAssembly="${StaticMethodAssemblyPath}">
<Output TaskParameter="Result" PropertyName="R"/>
</MSBuild.ExtensionPack.Framework.Assembly>
Generally, the most flexible option is to create a custom MSBuild task. This is all untested code meant to just to give you the idea:
In your msbuild file:
<UsingTask TaskName="FindFiles" AssemblyFile="FindFiles.dll" />
<!--
As you'll see below, SearchDirectory and SearchPatterns are input parameters,
MatchingFiles is an output parameter, SourceFiles is an ItemGroup assigned to
the output.
-->
<FindFiles SearchDirectory="$(MyDirectory)" SearchPatterns="*.dll;*.xml">
<Output ItemName="SourceFiles" TaskParameter="MatchingFiles" />
</FindFiles>
<!-- You can then use the generated ItemGroup output elsewhere. -->
<DocumentationSources>
<DocumentationSource sourceFile="#(SourceFiles)" xmlns="" />
</DocumentationSources>
FindFiles.cs:
using System;
using System.IO;
using System.Collections.Generic;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace FindFiles
{
public class FindFiles : Task
{
// input parameter
[Required]
public string SearchDirectory { get; set; }
// output parameter
[Required]
public string[] SearchPatterns { get; set; }
[Output]
public string[] MatchingFiles { get; private set; }
private bool ValidateParameters()
{
if (String.IsNullOrEmpty(SearchDirectory))
{
return false;
}
if (!Directory.Exists(SearchDirectory))
{
return false;
}
if (SearchPatterns == null || SearchPatterns.Length == 0)
{
return false;
}
return true;
}
// MSBuild tasks use the command pattern, this is where the magic happens,
// refactor as needed
public override bool Execute()
{
if (!ValidateParameters())
{
return false;
}
List<string> matchingFiles = new List<string>();
try
{
foreach (string searchPattern in SearchPatterns)
{
matchingFiles.AddRange(
Directory.GetFiles(SearchDirectory, searchPattern)
);
}
}
catch (IOException)
{
// it might be smarter to just let this exception fly, depending on
// how you want the task to behave
return false;
}
MatchingFiles = matchingFiles.ToArray();
return true;
}
}
}